From b5c8cc691e6cea280d64eba33d99db158c0010d7 Mon Sep 17 00:00:00 2001 From: Simone Pelosi Date: Fri, 22 May 2026 13:08:20 +0200 Subject: [PATCH 1/3] gh-127478: ftplib: prefer EPSV over PASV on IPv4 connections makepasv() now tries EPSV (RFC 2428) before PASV when connected over IPv4. EPSV returns only a port number without an IP address, making it transparent to firewall FTP Application Layer Gateways (ALGs) that intercept and often mangle PASV responses containing embedded IPs. Falls back to PASV if the server responds with an error to EPSV. A new class attribute FTP.prefer_epsv (default True) allows reverting to the old PASV-first behavior when set to False. This also fixes connectivity issues caused by the trust_server_pasv_ipv4_address security fix (bpo-43285): when firewalls rewrite PASV responses, clients connecting to the control channel IP on the data port often fail because nothing is listening there. EPSV avoids this entirely since the client always connects back to the same IP. --- Lib/ftplib.py | 18 ++++++++++++- Lib/test/test_ftplib.py | 25 +++++++++++++++++-- ...05-22-12-00-00.gh-issue-127478.EpsvFtp.rst | 6 +++++ 3 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-05-22-12-00-00.gh-issue-127478.EpsvFtp.rst diff --git a/Lib/ftplib.py b/Lib/ftplib.py index 2f092d50f31782b..ad31cea92343336 100644 --- a/Lib/ftplib.py +++ b/Lib/ftplib.py @@ -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, *, @@ -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 diff --git a/Lib/test/test_ftplib.py b/Lib/test/test_ftplib.py index f1eff9430f7351c..524303e136457b0 100644 --- a/Lib/test/test_ftplib.py +++ b/Lib/test/test_ftplib.py @@ -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) @@ -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( @@ -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( diff --git a/Misc/NEWS.d/next/Library/2026-05-22-12-00-00.gh-issue-127478.EpsvFtp.rst b/Misc/NEWS.d/next/Library/2026-05-22-12-00-00.gh-issue-127478.EpsvFtp.rst new file mode 100644 index 000000000000000..718ead97d9c9972 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-22-12-00-00.gh-issue-127478.EpsvFtp.rst @@ -0,0 +1,6 @@ +:mod:`ftplib`: :meth:`~ftplib.FTP.makepasv` now prefers 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. From 8e6ea0289905a0fe515d7bcffeb1e719491ef2a5 Mon Sep 17 00:00:00 2001 From: Simone Pelosi Date: Fri, 22 May 2026 13:24:12 +0200 Subject: [PATCH 2/3] Add documentation for FTP.prefer_epsv attribute --- Doc/library/ftplib.rst | 16 ++++++++++++++++ ...26-05-22-12-00-00.gh-issue-127478.EpsvFtp.rst | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Doc/library/ftplib.rst b/Doc/library/ftplib.rst index e1baeff3f373bf1..9acdf48cb863e27 100644 --- a/Doc/library/ftplib.rst +++ b/Doc/library/ftplib.rst @@ -276,6 +276,22 @@ FTP objects prints the line to :data:`sys.stdout`. + .. attribute:: FTP.prefer_epsv + + A :class:`bool` that controls whether :meth:`makepasv` tries 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, :meth:`makepasv` 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. diff --git a/Misc/NEWS.d/next/Library/2026-05-22-12-00-00.gh-issue-127478.EpsvFtp.rst b/Misc/NEWS.d/next/Library/2026-05-22-12-00-00.gh-issue-127478.EpsvFtp.rst index 718ead97d9c9972..fe08ce28e7299e4 100644 --- a/Misc/NEWS.d/next/Library/2026-05-22-12-00-00.gh-issue-127478.EpsvFtp.rst +++ b/Misc/NEWS.d/next/Library/2026-05-22-12-00-00.gh-issue-127478.EpsvFtp.rst @@ -1,4 +1,4 @@ -:mod:`ftplib`: :meth:`~ftplib.FTP.makepasv` now prefers EPSV (RFC 2428) over +: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 From 208011c89395a5d3a8981818201328b1cf6c02fc Mon Sep 17 00:00:00 2001 From: Simone Pelosi Date: Fri, 22 May 2026 14:14:51 +0200 Subject: [PATCH 3/3] Fix Sphinx nitpicky warnings in ftplib.rst Replace :meth:`makepasv` references (undocumented method) with plain prose to avoid unresolved cross-reference warnings in nitpicky mode. --- Doc/library/ftplib.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Doc/library/ftplib.rst b/Doc/library/ftplib.rst index 9acdf48cb863e27..27bafd8120a5098 100644 --- a/Doc/library/ftplib.rst +++ b/Doc/library/ftplib.rst @@ -278,14 +278,14 @@ FTP objects .. attribute:: FTP.prefer_epsv - A :class:`bool` that controls whether :meth:`makepasv` tries 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, :meth:`makepasv` falls back to PASV automatically. + 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.