Skip to content

Commit abbd6af

Browse files
Make ftplib not trust the PASV response. (#846)
1 parent 7ae608c commit abbd6af

File tree

2 files changed

+36
-4
lines changed

2 files changed

+36
-4
lines changed

Src/StdLib/Lib/ftplib.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,9 @@ class FTP:
107107
sock = None
108108
file = None
109109
welcome = None
110-
passiveserver = 1
110+
passiveserver = True
111+
# Disables https://bugs.python.org/issue43285 security if set to True.
112+
trust_server_pasv_ipv4_address = False
111113

112114
# Initialization method (called by class instantiation).
113115
# Initialize host to localhost, port to standard ftp port
@@ -310,8 +312,13 @@ def makeport(self):
310312
return sock
311313

312314
def makepasv(self):
315+
"""Internal: Does the PASV or EPSV handshake -> (address, port)"""
313316
if self.af == socket.AF_INET:
314-
host, port = parse227(self.sendcmd('PASV'))
317+
untrusted_host, port = parse227(self.sendcmd('PASV'))
318+
if self.trust_server_pasv_ipv4_address:
319+
host = untrusted_host
320+
else:
321+
host = self.sock.getpeername()[0]
315322
else:
316323
host, port = parse229(self.sendcmd('EPSV'), self.sock.getpeername())
317324
return host, port

Src/StdLib/Lib/test/test_ftplib.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ def __init__(self, conn):
6666
self.next_response = ''
6767
self.rest = None
6868
self.next_retr_data = RETR_DATA
69-
self.push('220 welcome')
69+
self.push('220 welcome')
70+
# We use this as the string IPv4 address to direct the client
71+
# to in response to a PASV command. To test security behavior.
72+
# https://bugs.python.org/issue43285/.
73+
self.fake_pasv_server_ip = '252.253.254.255'
7074

7175
def collect_incoming_data(self, data):
7276
self.in_buffer.append(data)
@@ -109,7 +113,8 @@ def cmd_pasv(self, arg):
109113
sock.bind((self.socket.getsockname()[0], 0))
110114
sock.listen(5)
111115
sock.settimeout(10)
112-
ip, port = sock.getsockname()[:2]
116+
port = sock.getsockname()[1]
117+
ip = self.fake_pasv_server_ip
113118
ip = ip.replace('.', ',')
114119
p1, p2 = divmod(port, 256)
115120
self.push('227 entering passive mode (%s,%d,%d)' %(ip, p1, p2))
@@ -576,6 +581,26 @@ def test_makepasv(self):
576581
conn.close()
577582
# IPv4 is in use, just make sure send_epsv has not been used
578583
self.assertEqual(self.server.handler_instance.last_received_cmd, 'pasv')
584+
585+
def test_makepasv_issue43285_security_disabled(self):
586+
"""Test the opt-in to the old vulnerable behavior."""
587+
self.client.trust_server_pasv_ipv4_address = True
588+
bad_host, port = self.client.makepasv()
589+
self.assertEqual(
590+
bad_host, self.server.handler_instance.fake_pasv_server_ip)
591+
# Opening and closing a connection keeps the dummy server happy
592+
# instead of timing out on accept.
593+
socket.create_connection((self.client.sock.getpeername()[0], port),
594+
timeout=TIMEOUT).close()
595+
596+
def test_makepasv_issue43285_security_enabled_default(self):
597+
self.assertFalse(self.client.trust_server_pasv_ipv4_address)
598+
trusted_host, port = self.client.makepasv()
599+
self.assertNotEqual(
600+
trusted_host, self.server.handler_instance.fake_pasv_server_ip)
601+
# Opening and closing a connection keeps the dummy server happy
602+
# instead of timing out on accept.
603+
socket.create_connection((trusted_host, port), timeout=TIMEOUT).close()
579604

580605
def test_line_too_long(self):
581606
self.assertRaises(ftplib.Error, self.client.sendcmd,

0 commit comments

Comments
 (0)