@@ -67,6 +67,10 @@ def __init__(self, conn):
6767 self .rest = None
6868 self .next_retr_data = RETR_DATA
6969 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,13 +113,13 @@ 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 ]
113- ip = ip . replace ( '.' , ',' )
114- p1 , p2 = divmod ( port , 256 )
116+ port = sock .getsockname ()[1 ]
117+ ip = self . fake_pasv_server_ip
118+ ip = ip . replace ( '.' , ',' ); p1 = port / 256 ; p2 = port % 256
115119 self .push ('227 entering passive mode (%s,%d,%d)' % (ip , p1 , p2 ))
116120 conn , addr = sock .accept ()
117121 self .dtp = self .dtp_handler (conn , baseclass = self )
118-
122+
119123 def cmd_eprt (self , arg ):
120124 af , ip , port = arg .split (arg [0 ])[1 :- 1 ]
121125 port = int (port )
@@ -577,6 +581,107 @@ def test_makepasv(self):
577581 # IPv4 is in use, just make sure send_epsv has not been used
578582 self .assertEqual (self .server .handler_instance .last_received_cmd , 'pasv' )
579583
584+ def test_makepasv_issue43285_security_disabled (self ):
585+ """Test the opt-in to the old vulnerable behavior."""
586+ self .client .trust_server_pasv_ipv4_address = True
587+ bad_host , port = self .client .makepasv ()
588+ self .assertEqual (
589+ bad_host , self .server .handler_instance .fake_pasv_server_ip )
590+ # Opening and closing a connection keeps the dummy server happy
591+ # instead of timing out on accept.
592+ socket .create_connection ((self .client .sock .getpeername ()[0 ], port ),
593+ timeout = TIMEOUT ).close ()
594+
595+ def test_makepasv_issue43285_security_enabled_default (self ):
596+ self .assertFalse (self .client .trust_server_pasv_ipv4_address )
597+ trusted_host , port = self .client .makepasv ()
598+ self .assertNotEqual (
599+ trusted_host , self .server .handler_instance .fake_pasv_server_ip )
600+ # Opening and closing a connection keeps the dummy server happy
601+ # instead of timing out on accept.
602+ socket .create_connection ((trusted_host , port ), timeout = TIMEOUT ).close ()
603+
604+ def test_with_statement (self ):
605+ self .client .quit ()
606+
607+ def is_client_connected ():
608+ if self .client .sock is None :
609+ return False
610+ try :
611+ self .client .sendcmd ('noop' )
612+ except (OSError , EOFError ):
613+ return False
614+ return True
615+
616+ # base test
617+ with ftplib .FTP (timeout = TIMEOUT ) as self .client :
618+ self .client .connect (self .server .host , self .server .port )
619+ self .client .sendcmd ('noop' )
620+ self .assertTrue (is_client_connected ())
621+ self .assertEqual (self .server .handler_instance .last_received_cmd , 'quit' )
622+ self .assertFalse (is_client_connected ())
623+
624+ # QUIT sent inside the with block
625+ with ftplib .FTP (timeout = TIMEOUT ) as self .client :
626+ self .client .connect (self .server .host , self .server .port )
627+ self .client .sendcmd ('noop' )
628+ self .client .quit ()
629+ self .assertEqual (self .server .handler_instance .last_received_cmd , 'quit' )
630+ self .assertFalse (is_client_connected ())
631+
632+ # force a wrong response code to be sent on QUIT: error_perm
633+ # is expected and the connection is supposed to be closed
634+ try :
635+ with ftplib .FTP (timeout = TIMEOUT ) as self .client :
636+ self .client .connect (self .server .host , self .server .port )
637+ self .client .sendcmd ('noop' )
638+ self .server .handler_instance .next_response = '550 error on quit'
639+ except ftplib .error_perm as err :
640+ self .assertEqual (str (err ), '550 error on quit' )
641+ else :
642+ self .fail ('Exception not raised' )
643+ # needed to give the threaded server some time to set the attribute
644+ # which otherwise would still be == 'noop'
645+ time .sleep (0.1 )
646+ self .assertEqual (self .server .handler_instance .last_received_cmd , 'quit' )
647+ self .assertFalse (is_client_connected ())
648+
649+ def test_source_address (self ):
650+ self .client .quit ()
651+ port = support .find_unused_port ()
652+ try :
653+ self .client .connect (self .server .host , self .server .port ,
654+ source_address = (HOST , port ))
655+ self .assertEqual (self .client .sock .getsockname ()[1 ], port )
656+ self .client .quit ()
657+ except OSError as e :
658+ if e .errno == errno .EADDRINUSE :
659+ self .skipTest ("couldn't bind to port %d" % port )
660+ raise
661+
662+ def test_source_address_passive_connection (self ):
663+ port = support .find_unused_port ()
664+ self .client .source_address = (HOST , port )
665+ try :
666+ with self .client .transfercmd ('list' ) as sock :
667+ self .assertEqual (sock .getsockname ()[1 ], port )
668+ except OSError as e :
669+ if e .errno == errno .EADDRINUSE :
670+ self .skipTest ("couldn't bind to port %d" % port )
671+ raise
672+
673+ def test_parse257 (self ):
674+ self .assertEqual (ftplib .parse257 ('257 "/foo/bar"' ), '/foo/bar' )
675+ self .assertEqual (ftplib .parse257 ('257 "/foo/bar" created' ), '/foo/bar' )
676+ self .assertEqual (ftplib .parse257 ('257 ""' ), '' )
677+ self .assertEqual (ftplib .parse257 ('257 "" created' ), '' )
678+ self .assertRaises (ftplib .error_reply , ftplib .parse257 , '250 "/foo/bar"' )
679+ # The 257 response is supposed to include the directory
680+ # name and in case it contains embedded double-quotes
681+ # they must be doubled (see RFC-959, chapter 7, appendix 2).
682+ self .assertEqual (ftplib .parse257 ('257 "/foo/b""ar"' ), '/foo/b"ar' )
683+ self .assertEqual (ftplib .parse257 ('257 "/foo/b""ar" created' ), '/foo/b"ar' )
684+
580685 def test_line_too_long (self ):
581686 self .assertRaises (ftplib .Error , self .client .sendcmd ,
582687 'x' * self .client .maxline * 2 )
0 commit comments