@@ -67,6 +67,10 @@ def __init__(self, conn):
67
67
self .rest = None
68
68
self .next_retr_data = RETR_DATA
69
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'
70
74
71
75
def collect_incoming_data (self , data ):
72
76
self .in_buffer .append (data )
@@ -109,13 +113,13 @@ def cmd_pasv(self, arg):
109
113
sock .bind ((self .socket .getsockname ()[0 ], 0 ))
110
114
sock .listen (5 )
111
115
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
115
119
self .push ('227 entering passive mode (%s,%d,%d)' % (ip , p1 , p2 ))
116
120
conn , addr = sock .accept ()
117
121
self .dtp = self .dtp_handler (conn , baseclass = self )
118
-
122
+
119
123
def cmd_eprt (self , arg ):
120
124
af , ip , port = arg .split (arg [0 ])[1 :- 1 ]
121
125
port = int (port )
@@ -577,6 +581,107 @@ def test_makepasv(self):
577
581
# IPv4 is in use, just make sure send_epsv has not been used
578
582
self .assertEqual (self .server .handler_instance .last_received_cmd , 'pasv' )
579
583
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
+
9E88
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
+
580
685
def test_line_too_long (self ):
581
686
self .assertRaises (ftplib .Error , self .client .sendcmd ,
582
687
'x' * self .client .maxline * 2 )
0 commit comments