Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Doc/library/ftplib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,22 @@ FTP objects
prints the line to :data:`sys.stdout`.


.. attribute:: FTP.prefer_epsv

A :class:`bool` that controls whether passive mode data connections
try the EPSV command (:rfc:`2428`) before falling back to PASV on IPv4
connections. Defaults to ``True``.

EPSV responses contain only a port number and no IP address, making them
transparent to firewall FTP Application Layer Gateways (ALGs) that
commonly intercept and mangle PASV responses. If the server does not
support EPSV, the connection falls back to PASV automatically.

Set to ``False`` to restore the legacy PASV-first behavior on IPv4.

.. versionadded:: 3.15


.. method:: FTP.set_pasv(val)

Enable "passive" mode if *val* is true, otherwise disable passive mode.
Expand Down
18 changes: 17 additions & 1 deletion Lib/ftplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ class FTP:
passiveserver = True
# Disables https://bugs.python.org/issue43285 security if set to True.
trust_server_pasv_ipv4_address = False
# Prefer EPSV (RFC 2428) over PASV on IPv4 connections.
# EPSV is firewall-transparent (no IP in response) and works on both
# IPv4 and IPv6. Falls back to PASV if server doesn't support EPSV.
prefer_epsv = True

def __init__(self, host='', user='', passwd='', acct='',
timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=None, *,
Expand Down Expand Up @@ -322,8 +326,20 @@ def makeport(self):
return sock

def makepasv(self):
"""Internal: Does the PASV or EPSV handshake -> (address, port)"""
"""Internal: Does the EPSV or PASV handshake -> (address, port)

Prefers EPSV (RFC 2428) on IPv4 when prefer_epsv is True, falling
back to PASV if the server does not support EPSV. EPSV is always
used on IPv6 regardless of prefer_epsv.
"""
if self.af == socket.AF_INET:
if self.prefer_epsv:
try:
host, port = parse229(self.sendcmd('EPSV'),
self.sock.getpeername())
return host, port
except error_perm:
pass
untrusted_host, port = parse227(self.sendcmd('PASV'))
if self.trust_server_pasv_ipv4_address:
host = untrusted_host
Expand Down
25 changes: 23 additions & 2 deletions Lib/test/test_ftplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def cmd_eprt(self, arg):

def cmd_epsv(self, arg):
with socket.create_server((self.socket.getsockname()[0], 0),
family=socket.AF_INET6) as sock:
family=self.socket.family) as sock:
sock.settimeout(TIMEOUT)
port = sock.getsockname()[1]
self.push('229 entering extended passive mode (|||%d|)' %port)
Expand Down Expand Up @@ -724,11 +724,31 @@ def test_makepasv(self):
host, port = self.client.makepasv()
conn = socket.create_connection((host, port), timeout=TIMEOUT)
conn.close()
# IPv4 is in use, just make sure send_epsv has not been used
# IPv4 with prefer_epsv=True (default) should use EPSV
self.assertEqual(self.server.handler_instance.last_received_cmd, 'epsv')

def test_makepasv_prefer_epsv_disabled(self):
self.client.prefer_epsv = False
host, port = self.client.makepasv()
conn = socket.create_connection((host, port), timeout=TIMEOUT)
conn.close()
self.assertEqual(self.server.handler_instance.last_received_cmd, 'pasv')

def test_makepasv_prefer_epsv_fallback_to_pasv(self):
# Simulate server not supporting EPSV by monkey-patching the handler
original_cmd_epsv = self.server.handler.cmd_epsv
self.server.handler.cmd_epsv = lambda self_handler, arg: self_handler.push('500 EPSV not understood')
try:
host, port = self.client.makepasv()
conn = socket.create_connection((host, port), timeout=TIMEOUT)
conn.close()
self.assertEqual(self.server.handler_instance.last_received_cmd, 'pasv')
finally:
self.server.handler.cmd_epsv = original_cmd_epsv

def test_makepasv_issue43285_security_disabled(self):
"""Test the opt-in to the old vulnerable behavior."""
self.client.prefer_epsv = False
self.client.trust_server_pasv_ipv4_address = True
bad_host, port = self.client.makepasv()
self.assertEqual(
Expand All @@ -739,6 +759,7 @@ def test_makepasv_issue43285_security_disabled(self):
timeout=TIMEOUT).close()

def test_makepasv_issue43285_security_enabled_default(self):
self.client.prefer_epsv = False
self.assertFalse(self.client.trust_server_pasv_ipv4_address)
trusted_host, port = self.client.makepasv()
self.assertNotEqual(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
:mod:`ftplib`: Passive mode data connections now prefer EPSV (RFC 2428) over
PASV on IPv4 connections, falling back to PASV if the server does not support
EPSV. EPSV is firewall-transparent as it does not embed an IP address in the
response, avoiding interference from firewall FTP Application Layer Gateways
(ALGs). A new class attribute :attr:`~ftplib.FTP.prefer_epsv` (default
``True``) controls this behavior.
Loading