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