gh-87451: Apply CVE-2021-4189 PASV fix to ftplib.ftpcp() (GH-149648) · python/cpython@eac4fe3

GitHub

@@ -16,7 +16,7 @@

1616exceptImportError:

1717ssl=None

181819-fromunittestimportTestCase, skipUnless

19+fromunittestimportmock, TestCase, skipUnless

2020fromtestimportsupport

2121fromtest.supportimportrequires_subprocess

2222fromtest.supportimportthreading_helper

@@ -1145,6 +1145,40 @@ def testTimeoutDirectAccess(self):

11451145ftp.close()

11461146114711471148+classTestFtpcpSecurity(TestCase):

1149+"""ftpcp() must not trust the host a source server advertises in PASV.

1150+1151+ A malicious source server can otherwise redirect the target server's

1152+ data connection to an arbitrary host:port (SSRF), so ftpcp() uses the

1153+ source server's actual peer address instead, the same as FTP.makepasv().

1154+ """

1155+1156+def_make_pair(self, *, advertised_host, real_host, trust=False):

1157+source=mock.Mock(spec=ftplib.FTP)

1158+source.trust_server_pasv_ipv4_address=trust

1159+source.sock.getpeername.return_value= (real_host, 21)

1160+# PASV replies give the host as comma-separated octets, not dotted.

1161+advertised=advertised_host.replace('.', ',')

1162+source.sendcmd.side_effect=lambdacmd: (

1163+f'227 Entering Passive Mode ({advertised},1,2).'

1164+ifcmd=='PASV'else'150 ok')

1165+target=mock.Mock(spec=ftplib.FTP)

1166+target.sendcmd.return_value='150 ok'

1167+returnsource, target

1168+1169+deftest_ftpcp_ignores_untrusted_pasv_host(self):

1170+source, target=self._make_pair(advertised_host='10.0.0.5',

1171+real_host='198.51.100.7')

1172+ftplib.ftpcp(source, 'a', target, 'b')

1173+target.sendport.assert_called_once_with('198.51.100.7', 258)

1174+1175+deftest_ftpcp_trust_server_pasv_ipv4_address(self):

1176+source, target=self._make_pair(advertised_host='10.0.0.5',

1177+real_host='198.51.100.7', trust=True)

1178+ftplib.ftpcp(source, 'a', target, 'b')

1179+target.sendport.assert_called_once_with('10.0.0.5', 258)

1180+1181+11481182classMiscTestCase(TestCase):

11491183deftest__all__(self):

11501184not_exported= {