diff --git a/Doc/library/ftplib.rst b/Doc/library/ftplib.rst index bb15322067245e..456ceae38007d3 100644 --- a/Doc/library/ftplib.rst +++ b/Doc/library/ftplib.rst @@ -84,6 +84,14 @@ FTP objects .. class:: FTP(host='', user='', passwd='', acct='', timeout=None, \ source_address=None, *, encoding='utf-8') + .. warning:: + + Use of this class may create a vulnerability to + `man-in-the-middle attack `_, + please consider using the :class:`FTP_TLS` class, and reflect + on your `threat model `_ + before using an unprotected FTP connection. + Return a new instance of the :class:`FTP` class. :param str host: @@ -454,10 +462,6 @@ FTP_TLS objects Connect to port 21 implicitly securing the FTP control connection before authenticating. - .. note:: - The user must explicitly secure the data connection - by calling the :meth:`prot_p` method. - :param str host: The hostname to connect to. If given, :code:`connect(host)` is implicitly called by the constructor. @@ -516,8 +520,6 @@ FTP_TLS objects >>> ftps = FTP_TLS('ftp.pureftpd.org') >>> ftps.login() '230 Anonymous user logged in' - >>> ftps.prot_p() - '200 Data protection level set to "private"' >>> ftps.nlst() ['6jack', 'OpenBSD', 'antilink', 'blogbench', 'bsdcam', 'clockspeed', 'djbdns-jedi', 'docs', 'eaccelerator-jedi', 'favicon.ico', 'francotone', 'fugu', 'ignore', 'libpuzzle', 'metalog', 'minidentd', 'misc', 'mysql-udf-global-user-variables', 'php-jenkins-hash', 'php-skein-hash', 'php-webdav', 'phpaudit', 'phpbench', 'pincaster', 'ping', 'posto', 'pub', 'public', 'public_keys', 'pure-ftpd', 'qscan', 'qtc', 'sharedance', 'skycache', 'sound', 'tmp', 'ucarp'] @@ -548,11 +550,18 @@ FTP_TLS objects .. method:: FTP_TLS.prot_p() - Set up secure data connection. + Set up secure data connection (with TLS). .. method:: FTP_TLS.prot_c() - Set up clear text data connection. + Set up clear text data connection (without TLS). + + .. warning:: + + Calling this method may create a vulnerability to + `man-in-the-middle attack `_. + Please reflect on your `threat model `_ + before requesting clear text data connection without TLS. Module variables diff --git a/Lib/ftplib.py b/Lib/ftplib.py index 640acc64f620cc..ac93b703efb1b3 100644 --- a/Lib/ftplib.py +++ b/Lib/ftplib.py @@ -681,9 +681,6 @@ class FTP_TLS(FTP): Connect as usual to port 21 implicitly securing the FTP control connection before authenticating. - Securing the data connection requires user to explicitly ask - for it by calling prot_p() method. - Usage example: >>> from ftplib import FTP_TLS >>> ftps = FTP_TLS('ftp.python.org') @@ -733,6 +730,7 @@ def auth(self): resp = self.voidcmd('AUTH SSL') self.sock = self.context.wrap_socket(self.sock, server_hostname=self.host) self.file = self.sock.makefile(mode='r', encoding=self.encoding) + self.prot_p() return resp def ccc(self): diff --git a/Lib/test/test_ftplib.py b/Lib/test/test_ftplib.py index c864d401f9ed67..cfc75c6c49b006 100644 --- a/Lib/test/test_ftplib.py +++ b/Lib/test/test_ftplib.py @@ -917,7 +917,6 @@ def setUp(self, encoding=DEFAULT_ENCODING): self.client.connect(self.server.host, self.server.port) # enable TLS self.client.auth() - self.client.prot_p() @skipUnless(ssl, "SSL not available") @@ -944,15 +943,9 @@ def test_control_connection(self): self.assertIsInstance(self.client.sock, ssl.SSLSocket) def test_data_connection(self): - # clear text - with self.client.transfercmd('list') as sock: - self.assertNotIsInstance(sock, ssl.SSLSocket) - self.assertEqual(sock.recv(1024), - LIST_DATA.encode(self.client.encoding)) - self.assertEqual(self.client.voidresp(), "226 transfer complete") + self.client.login() - # secured, after PROT P - self.client.prot_p() + # secured with self.client.transfercmd('list') as sock: self.assertIsInstance(sock, ssl.SSLSocket) # consume from SSL socket to finalize handshake and avoid @@ -961,7 +954,7 @@ def test_data_connection(self): LIST_DATA.encode(self.client.encoding)) self.assertEqual(self.client.voidresp(), "226 transfer complete") - # PROT C is issued, the connection must be in cleartext again + # PROT C is issued, the connection must be in cleartext self.client.prot_c() with self.client.transfercmd('list') as sock: self.assertNotIsInstance(sock, ssl.SSLSocket) @@ -1000,7 +993,6 @@ def test_context(self): self.assertIs(self.client.sock.context, ctx) self.assertIsInstance(self.client.sock, ssl.SSLSocket) - self.client.prot_p() with self.client.transfercmd('list') as sock: self.assertIs(sock.context, ctx) self.assertIsInstance(sock, ssl.SSLSocket) @@ -1028,7 +1020,6 @@ def test_check_hostname(self): # exception quits connection self.client.connect(self.server.host, self.server.port) - self.client.prot_p() with self.assertRaises(ssl.CertificateError): with self.client.transfercmd("list") as sock: pass @@ -1039,7 +1030,6 @@ def test_check_hostname(self): self.client.quit() self.client.connect("localhost", self.server.port) - self.client.prot_p() with self.client.transfercmd("list") as sock: pass diff --git a/Misc/NEWS.d/next/Library/2026-01-06-21-58-12.gh-issue-143497.k3XVrU.rst b/Misc/NEWS.d/next/Library/2026-01-06-21-58-12.gh-issue-143497.k3XVrU.rst new file mode 100644 index 00000000000000..6f43bbcf9d2c17 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-06-21-58-12.gh-issue-143497.k3XVrU.rst @@ -0,0 +1,5 @@ +Fix defaults of :class:`ftplib.FTP_TLS` to not leave the data connection +without TLS and vulnerable to `man-in-the-middle attack +`_. Also extend the +documenation of :class:`ftplib.FTP_TLS` and :meth:`ftplib.FTP_TLS.prot_c` +to warn about the security implications. +Patch by Sebastian Pipping.