]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.15] gh-87451: Apply CVE-2021-4189 PASV fix to ftplib.ftpcp() (GH-149648) (#149792)
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Fri, 15 May 2026 10:50:45 +0000 (12:50 +0200)
committerGitHub <noreply@github.com>
Fri, 15 May 2026 10:50:45 +0000 (10:50 +0000)
gh-87451: Apply CVE-2021-4189 PASV fix to ftplib.ftpcp() (GH-149648)

ftpcp() called parse227() directly and passed the source server's
self-reported PASV IPv4 address to the target server's PORT command,
bypassing the CVE-2021-4189 fix that was applied only to FTP.makepasv().
A malicious source FTP server could use this to redirect the target
server's data connection to an arbitrary host:port (SSRF).

ftpcp() now uses the source server's actual peer address, honoring the
existing trust_server_pasv_ipv4_address opt-out, the same as makepasv().

Thanks to Qi Ding at Aurascape AI for the report. (GHSA-w8c5-q2xf-gf7c)
(cherry picked from commit eac4fe3b2c77693790a5ef7dfab127c1fee81bf9)

Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com>
Lib/ftplib.py
Lib/test/test_ftplib.py
Misc/NEWS.d/next/Security/2026-05-10-18-05-32.gh-issue-87451.XkKB6M.rst [new file with mode: 0644]

index 640acc64f620cc9e05912d8a3fc1cd5e0be32f2b..2f092d50f31782bccc32a372e362c715b883d64d 100644 (file)
@@ -883,7 +883,16 @@ def ftpcp(source, sourcename, target, targetname = '', type = 'I'):
     type = 'TYPE ' + type
     source.voidcmd(type)
     target.voidcmd(type)
-    sourcehost, sourceport = parse227(source.sendcmd('PASV'))
+    # Don't trust the IPv4 address the source server advertises in its PASV
+    # reply: a malicious source could otherwise point the target's data
+    # connection at an arbitrary host (SSRF).  A caller that needs the old
+    # behavior can set trust_server_pasv_ipv4_address on the source FTP
+    # object.  See FTP.makepasv(), which applies the same rule.
+    untrusted_host, sourceport = parse227(source.sendcmd('PASV'))
+    if source.trust_server_pasv_ipv4_address:
+        sourcehost = untrusted_host
+    else:
+        sourcehost = source.sock.getpeername()[0]
     target.sendport(sourcehost, sourceport)
     # RFC 959: the user must "listen" [...] BEFORE sending the
     # transfer request.
index c864d401f9ed67ede6a887edad8059e29c9384ff..f1eff9430f7351c4906b696c4440c8875d728f92 100644 (file)
@@ -16,7 +16,7 @@ try:
 except ImportError:
     ssl = None
 
-from unittest import TestCase, skipUnless
+from unittest import mock, TestCase, skipUnless
 from test import support
 from test.support import requires_subprocess
 from test.support import threading_helper
@@ -1145,6 +1145,40 @@ class TestTimeouts(TestCase):
         ftp.close()
 
 
+class TestFtpcpSecurity(TestCase):
+    """ftpcp() must not trust the host a source server advertises in PASV.
+
+    A malicious source server can otherwise redirect the target server's
+    data connection to an arbitrary host:port (SSRF), so ftpcp() uses the
+    source server's actual peer address instead, the same as FTP.makepasv().
+    """
+
+    def _make_pair(self, *, advertised_host, real_host, trust=False):
+        source = mock.Mock(spec=ftplib.FTP)
+        source.trust_server_pasv_ipv4_address = trust
+        source.sock.getpeername.return_value = (real_host, 21)
+        # PASV replies give the host as comma-separated octets, not dotted.
+        advertised = advertised_host.replace('.', ',')
+        source.sendcmd.side_effect = lambda cmd: (
+            f'227 Entering Passive Mode ({advertised},1,2).'
+            if cmd == 'PASV' else '150 ok')
+        target = mock.Mock(spec=ftplib.FTP)
+        target.sendcmd.return_value = '150 ok'
+        return source, target
+
+    def test_ftpcp_ignores_untrusted_pasv_host(self):
+        source, target = self._make_pair(advertised_host='10.0.0.5',
+                                         real_host='198.51.100.7')
+        ftplib.ftpcp(source, 'a', target, 'b')
+        target.sendport.assert_called_once_with('198.51.100.7', 258)
+
+    def test_ftpcp_trust_server_pasv_ipv4_address(self):
+        source, target = self._make_pair(advertised_host='10.0.0.5',
+                                         real_host='198.51.100.7', trust=True)
+        ftplib.ftpcp(source, 'a', target, 'b')
+        target.sendport.assert_called_once_with('10.0.0.5', 258)
+
+
 class MiscTestCase(TestCase):
     def test__all__(self):
         not_exported = {
diff --git a/Misc/NEWS.d/next/Security/2026-05-10-18-05-32.gh-issue-87451.XkKB6M.rst b/Misc/NEWS.d/next/Security/2026-05-10-18-05-32.gh-issue-87451.XkKB6M.rst
new file mode 100644 (file)
index 0000000..21a79c3
--- /dev/null
@@ -0,0 +1,6 @@
+The :mod:`ftplib` module's undocumented ``ftpcp`` function no longer trusts
+the IPv4 address value returned from the source server in response to the
+``PASV`` command by default, completing the fix for CVE-2021-4189.  As with
+:class:`ftplib.FTP`, the former behavior can be re-enabled by setting the
+``trust_server_pasv_ipv4_address`` attribute on the source :class:`ftplib.FTP`
+instance to ``True``.  Thanks to Qi Deng at Aurascape AI for the report.