157634
commit be5a83e84a34091f2a4e3c6dfb911b20e78e690c
157634
Author: usa <usa@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>
157634
Date:   Wed Jul 7 10:34:08 2021 +0000
157634
157634
    Ignore IP addresses in PASV responses by default, and add new option use_pasv_ip
157634
    
157634
    This fixes CVE-2021-31810.
157634
    Reported by Alexandr Savca.
157634
    
157634
    Co-authored-by: Shugo Maeda <shugo@ruby-lang.org>
157634
    
157634
    
157634
    git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/branches/ruby_2_6@67949 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
157634
157634
diff --git a/lib/net/ftp.rb b/lib/net/ftp.rb
157634
index e68d825dcf..c5d669d898 100644
157634
--- a/lib/net/ftp.rb
157634
+++ b/lib/net/ftp.rb
157634
@@ -97,6 +97,10 @@ class FTP < Protocol
157634
     # When +true+, the connection is in passive mode.  Default: +true+.
157634
     attr_accessor :passive
157634
 
157634
+    # When +true+, use the IP address in PASV responses.  Otherwise, it uses
157634
+    # the same IP address for the control connection.  Default: +false+.
157634
+    attr_accessor :use_pasv_ip
157634
+
157634
     # When +true+, all traffic to and from the server is written
157634
     # to +$stdout+.  Default: +false+.
157634
     attr_accessor :debug_mode
157634
@@ -205,6 +209,9 @@ def FTP.open(host, *args)
157634
     #                          handshake.
157634
     #                          See Net::FTP#ssl_handshake_timeout for
157634
     #                          details.  Default: +nil+.
157634
+    # use_pasv_ip::  When +true+, use the IP address in PASV responses.
157634
+    #                Otherwise, it uses the same IP address for the control
157634
+    #                connection.  Default: +false+.
157634
     # debug_mode::  When +true+, all traffic to and from the server is
157634
     #               written to +$stdout+.  Default: +false+.
157634
     #
157634
@@ -265,6 +272,7 @@ def initialize(host = nil, user_or_options = {}, passwd = nil, acct = nil)
157634
       @open_timeout = options[:open_timeout]
157634
       @ssl_handshake_timeout = options[:ssl_handshake_timeout]
157634
       @read_timeout = options[:read_timeout] || 60
157634
+      @use_pasv_ip = options[:use_pasv_ip] || false
157634
       if host
157634
         connect(host, options[:port] || FTP_PORT)
157634
         if options[:username]
157634
@@ -1330,7 +1338,12 @@ def parse227(resp) # :nodoc:
157634
         raise FTPReplyError, resp
157634
       end
157634
       if m = /\((?<host>\d+(,\d+){3}),(?<port>\d+,\d+)\)/.match(resp)
157634
-        return parse_pasv_ipv4_host(m["host"]), parse_pasv_port(m["port"])
157634
+        if @use_pasv_ip
157634
+          host = parse_pasv_ipv4_host(m["host"])
157634
+        else
157634
+          host = @bare_sock.remote_address.ip_address
157634
+        end
157634
+        return host, parse_pasv_port(m["port"])
157634
       else
157634
         raise FTPProtoError, resp
157634
       end
157634
diff --git a/test/net/ftp/test_ftp.rb b/test/net/ftp/test_ftp.rb
157634
index a5219644bb..b3fe7774ed 100644
157634
--- a/test/net/ftp/test_ftp.rb
157634
+++ b/test/net/ftp/test_ftp.rb
157634
@@ -61,7 +61,7 @@ def test_connect_fail
157634
   end
157634
 
157634
   def test_parse227
157634
-    ftp = Net::FTP.new
157634
+    ftp = Net::FTP.new(nil, use_pasv_ip: true)
157634
     host, port = ftp.send(:parse227, "227 Entering Passive Mode (192,168,0,1,12,34)")
157634
     assert_equal("192.168.0.1", host)
157634
     assert_equal(3106, port)
157634
@@ -80,6 +80,14 @@ def test_parse227
157634
     assert_raise(Net::FTPProtoError) do
157634
       ftp.send(:parse227, "227 ) foo bar (")
157634
     end
157634
+
157634
+    ftp = Net::FTP.new
157634
+    sock = OpenStruct.new
157634
+    sock.remote_address = OpenStruct.new
157634
+    sock.remote_address.ip_address = "10.0.0.1"
157634
+    ftp.instance_variable_set(:@bare_sock, sock)
157634
+    host, port = ftp.send(:parse227, "227 Entering Passive Mode (192,168,0,1,12,34)")
157634
+    assert_equal("10.0.0.1", host)
157634
   end
157634
 
157634
   def test_parse228
157634
@@ -2360,10 +2368,155 @@ def test_puttextfile_command_injection
157634
     end
157634
   end
157634
 
157634
+  def test_ignore_pasv_ip
157634
+    commands = []
157634
+    binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3
157634
+    server = create_ftp_server(nil, "127.0.0.1") { |sock|
157634
+      sock.print("220 (test_ftp).\r\n")
157634
+      commands.push(sock.gets)
157634
+      sock.print("331 Please specify the password.\r\n")
157634
+      commands.push(sock.gets)
157634
+      sock.print("230 Login successful.\r\n")
157634
+      commands.push(sock.gets)
157634
+      sock.print("200 Switching to Binary mode.\r\n")
157634
+      line = sock.gets
157634
+      commands.push(line)
157634
+      data_server = TCPServer.new("127.0.0.1", 0)
157634
+      port = data_server.local_address.ip_port
157634
+      sock.printf("227 Entering Passive Mode (999,0,0,1,%s).\r\n",
157634
+                  port.divmod(256).join(","))
157634
+      commands.push(sock.gets)
157634
+      sock.print("150 Opening BINARY mode data connection for foo (#{binary_data.size} bytes)\r\n")
157634
+      conn = data_server.accept
157634
+      binary_data.scan(/.{1,1024}/nm) do |s|
157634
+        conn.print(s)
157634
+      end
157634
+      conn.shutdown(Socket::SHUT_WR)
157634
+      conn.read
157634
+      conn.close
157634
+      data_server.close
157634
+      sock.print("226 Transfer complete.\r\n")
157634
+    }
157634
+    begin
157634
+      begin
157634
+        ftp = Net::FTP.new
157634
+        ftp.passive = true
157634
+        ftp.read_timeout *= 5 if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? # for --jit-wait
157634
+        ftp.connect("127.0.0.1", server.port)
157634
+        ftp.login
157634
+        assert_match(/\AUSER /, commands.shift)
157634
+        assert_match(/\APASS /, commands.shift)
157634
+        assert_equal("TYPE I\r\n", commands.shift)
157634
+        buf = ftp.getbinaryfile("foo", nil)
157634
+        assert_equal(binary_data, buf)
157634
+        assert_equal(Encoding::ASCII_8BIT, buf.encoding)
157634
+        assert_equal("PASV\r\n", commands.shift)
157634
+        assert_equal("RETR foo\r\n", commands.shift)
157634
+        assert_equal(nil, commands.shift)
157634
+      ensure
157634
+        ftp.close if ftp
157634
+      end
157634
+    ensure
157634
+      server.close
157634
+    end
157634
+  end
157634
+
157634
+  def test_use_pasv_ip
157634
+    commands = []
157634
+    binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3
157634
+    server = create_ftp_server(nil, "127.0.0.1") { |sock|
157634
+      sock.print("220 (test_ftp).\r\n")
157634
+      commands.push(sock.gets)
157634
+      sock.print("331 Please specify the password.\r\n")
157634
+      commands.push(sock.gets)
157634
+      sock.print("230 Login successful.\r\n")
157634
+      commands.push(sock.gets)
157634
+      sock.print("200 Switching to Binary mode.\r\n")
157634
+      line = sock.gets
157634
+      commands.push(line)
157634
+      data_server = TCPServer.new("127.0.0.1", 0)
157634
+      port = data_server.local_address.ip_port
157634
+      sock.printf("227 Entering Passive Mode (127,0,0,1,%s).\r\n",
157634
+                  port.divmod(256).join(","))
157634
+      commands.push(sock.gets)
157634
+      sock.print("150 Opening BINARY mode data connection for foo (#{binary_data.size} bytes)\r\n")
157634
+      conn = data_server.accept
157634
+      binary_data.scan(/.{1,1024}/nm) do |s|
157634
+        conn.print(s)
157634
+      end
157634
+      conn.shutdown(Socket::SHUT_WR)
157634
+      conn.read
157634
+      conn.close
157634
+      data_server.close
157634
+      sock.print("226 Transfer complete.\r\n")
157634
+    }
157634
+    begin
157634
+      begin
157634
+        ftp = Net::FTP.new
157634
+        ftp.passive = true
157634
+        ftp.use_pasv_ip = true
157634
+        ftp.read_timeout *= 5 if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? # for --jit-wait
157634
+        ftp.connect("127.0.0.1", server.port)
157634
+        ftp.login
157634
+        assert_match(/\AUSER /, commands.shift)
157634
+        assert_match(/\APASS /, commands.shift)
157634
+        assert_equal("TYPE I\r\n", commands.shift)
157634
+        buf = ftp.getbinaryfile("foo", nil)
157634
+        assert_equal(binary_data, buf)
157634
+        assert_equal(Encoding::ASCII_8BIT, buf.encoding)
157634
+        assert_equal("PASV\r\n", commands.shift)
157634
+        assert_equal("RETR foo\r\n", commands.shift)
157634
+        assert_equal(nil, commands.shift)
157634
+      ensure
157634
+        ftp.close if ftp
157634
+      end
157634
+    ensure
157634
+      server.close
157634
+    end
157634
+  end
157634
+
157634
+  def test_use_pasv_invalid_ip
157634
+    commands = []
157634
+    binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3
157634
+    server = create_ftp_server(nil, "127.0.0.1") { |sock|
157634
+      sock.print("220 (test_ftp).\r\n")
157634
+      commands.push(sock.gets)
157634
+      sock.print("331 Please specify the password.\r\n")
157634
+      commands.push(sock.gets)
157634
+      sock.print("230 Login successful.\r\n")
157634
+      commands.push(sock.gets)
157634
+      sock.print("200 Switching to Binary mode.\r\n")
157634
+      line = sock.gets
157634
+      commands.push(line)
157634
+      sock.print("227 Entering Passive Mode (999,0,0,1,48,57).\r\n")
157634
+      commands.push(sock.gets)
157634
+    }
157634
+    begin
157634
+      begin
157634
+        ftp = Net::FTP.new
157634
+        ftp.passive = true
157634
+        ftp.use_pasv_ip = true
157634
+        ftp.read_timeout *= 5 if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? # for --jit-wait
157634
+        ftp.connect("127.0.0.1", server.port)
157634
+        ftp.login
157634
+        assert_match(/\AUSER /, commands.shift)
157634
+        assert_match(/\APASS /, commands.shift)
157634
+        assert_equal("TYPE I\r\n", commands.shift)
157634
+        assert_raise(SocketError) do
157634
+          ftp.getbinaryfile("foo", nil)
157634
+        end
157634
+      ensure
157634
+        ftp.close if ftp
157634
+      end
157634
+    ensure
157634
+      server.close
157634
+    end
157634
+  end
157634
+
157634
   private
157634
 
157634
-  def create_ftp_server(sleep_time = nil)
157634
-    server = TCPServer.new(SERVER_ADDR, 0)
157634
+  def create_ftp_server(sleep_time = nil, addr = SERVER_ADDR)
157634
+    server = TCPServer.new(addr, 0)
157634
     @thread = Thread.start do
157634
       if sleep_time
157634
         sleep(sleep_time)