Skip to content

Commit bf4d051

Browse files
mameshugo
authored andcommitted
Ignore IP addresses in PASV responses by default, and add new option use_pasv_ip
This fixes CVE-2021-31810. Reported by Alexandr Savca. Co-authored-by: Shugo Maeda <shugo@ruby-lang.org>
1 parent 1545d4f commit bf4d051

2 files changed

Lines changed: 170 additions & 4 deletions

File tree

lib/net/ftp.rb

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ class FTP < Protocol
9898
# When +true+, the connection is in passive mode. Default: +true+.
9999
attr_accessor :passive
100100

101+
# When +true+, use the IP address in PASV responses. Otherwise, it uses
102+
# the same IP address for the control connection. Default: +false+.
103+
attr_accessor :use_pasv_ip
104+
101105
# When +true+, all traffic to and from the server is written
102106
# to +$stdout+. Default: +false+.
103107
attr_accessor :debug_mode
@@ -206,6 +210,9 @@ def FTP.open(host, *args)
206210
# handshake.
207211
# See Net::FTP#ssl_handshake_timeout for
208212
# details. Default: +nil+.
213+
# use_pasv_ip:: When +true+, use the IP address in PASV responses.
214+
# Otherwise, it uses the same IP address for the control
215+
# connection. Default: +false+.
209216
# debug_mode:: When +true+, all traffic to and from the server is
210217
# written to +$stdout+. Default: +false+.
211218
#
@@ -266,6 +273,7 @@ def initialize(host = nil, user_or_options = {}, passwd = nil, acct = nil)
266273
@open_timeout = options[:open_timeout]
267274
@ssl_handshake_timeout = options[:ssl_handshake_timeout]
268275
@read_timeout = options[:read_timeout] || 60
276+
@use_pasv_ip = options[:use_pasv_ip] || false
269277
if host
270278
connect(host, options[:port] || FTP_PORT)
271279
if options[:username]
@@ -1381,7 +1389,12 @@ def parse227(resp) # :nodoc:
13811389
raise FTPReplyError, resp
13821390
end
13831391
if m = /\((?<host>\d+(?:,\d+){3}),(?<port>\d+,\d+)\)/.match(resp)
1384-
return parse_pasv_ipv4_host(m["host"]), parse_pasv_port(m["port"])
1392+
if @use_pasv_ip
1393+
host = parse_pasv_ipv4_host(m["host"])
1394+
else
1395+
host = @bare_sock.remote_address.ip_address
1396+
end
1397+
return host, parse_pasv_port(m["port"])
13851398
else
13861399
raise FTPProtoError, resp
13871400
end

test/net/ftp/test_ftp.rb

Lines changed: 156 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def test_connect_fail
6161
end
6262

6363
def test_parse227
64-
ftp = Net::FTP.new
64+
ftp = Net::FTP.new(nil, use_pasv_ip: true)
6565
host, port = ftp.send(:parse227, "227 Entering Passive Mode (192,168,0,1,12,34)")
6666
assert_equal("192.168.0.1", host)
6767
assert_equal(3106, port)
@@ -80,6 +80,14 @@ def test_parse227
8080
assert_raise(Net::FTPProtoError) do
8181
ftp.send(:parse227, "227 ) foo bar (")
8282
end
83+
84+
ftp = Net::FTP.new
85+
sock = OpenStruct.new
86+
sock.remote_address = OpenStruct.new
87+
sock.remote_address.ip_address = "10.0.0.1"
88+
ftp.instance_variable_set(:@bare_sock, sock)
89+
host, port = ftp.send(:parse227, "227 Entering Passive Mode (192,168,0,1,12,34)")
90+
assert_equal("10.0.0.1", host)
8391
end
8492

8593
def test_parse228
@@ -2525,10 +2533,155 @@ def test_time_parser
25252533
assert_equal("invalid time-val: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...", e.message)
25262534
end
25272535

2536+
def test_ignore_pasv_ip
2537+
commands = []
2538+
binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3
2539+
server = create_ftp_server(nil, "127.0.0.1") { |sock|
2540+
sock.print("220 (test_ftp).\r\n")
2541+
commands.push(sock.gets)
2542+
sock.print("331 Please specify the password.\r\n")
2543+
commands.push(sock.gets)
2544+
sock.print("230 Login successful.\r\n")
2545+
commands.push(sock.gets)
2546+
sock.print("200 Switching to Binary mode.\r\n")
2547+
line = sock.gets
2548+
commands.push(line)
2549+
data_server = TCPServer.new("127.0.0.1", 0)
2550+
port = data_server.local_address.ip_port
2551+
sock.printf("227 Entering Passive Mode (999,0,0,1,%s).\r\n",
2552+
port.divmod(256).join(","))
2553+
commands.push(sock.gets)
2554+
sock.print("150 Opening BINARY mode data connection for foo (#{binary_data.size} bytes)\r\n")
2555+
conn = data_server.accept
2556+
binary_data.scan(/.{1,1024}/nm) do |s|
2557+
conn.print(s)
2558+
end
2559+
conn.shutdown(Socket::SHUT_WR)
2560+
conn.read
2561+
conn.close
2562+
data_server.close
2563+
sock.print("226 Transfer complete.\r\n")
2564+
}
2565+
begin
2566+
begin
2567+
ftp = Net::FTP.new
2568+
ftp.passive = true
2569+
ftp.read_timeout *= 5 if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? # for --jit-wait
2570+
ftp.connect("127.0.0.1", server.port)
2571+
ftp.login
2572+
assert_match(/\AUSER /, commands.shift)
2573+
assert_match(/\APASS /, commands.shift)
2574+
assert_equal("TYPE I\r\n", commands.shift)
2575+
buf = ftp.getbinaryfile("foo", nil)
2576+
assert_equal(binary_data, buf)
2577+
assert_equal(Encoding::ASCII_8BIT, buf.encoding)
2578+
assert_equal("PASV\r\n", commands.shift)
2579+
assert_equal("RETR foo\r\n", commands.shift)
2580+
assert_equal(nil, commands.shift)
2581+
ensure
2582+
ftp.close if ftp
2583+
end
2584+
ensure
2585+
server.close
2586+
end
2587+
end
2588+
2589+
def test_use_pasv_ip
2590+
commands = []
2591+
binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3
2592+
server = create_ftp_server(nil, "127.0.0.1") { |sock|
2593+
sock.print("220 (test_ftp).\r\n")
2594+
commands.push(sock.gets)
2595+
sock.print("331 Please specify the password.\r\n")
2596+
commands.push(sock.gets)
2597+
sock.print("230 Login successful.\r\n")
2598+
commands.push(sock.gets)
2599+
sock.print("200 Switching to Binary mode.\r\n")
2600+
line = sock.gets
2601+
commands.push(line)
2602+
data_server = TCPServer.new("127.0.0.1", 0)
2603+
port = data_server.local_address.ip_port
2604+
sock.printf("227 Entering Passive Mode (127,0,0,1,%s).\r\n",
2605+
port.divmod(256).join(","))
2606+
commands.push(sock.gets)
2607+
sock.print("150 Opening BINARY mode data connection for foo (#{binary_data.size} bytes)\r\n")
2608+
conn = data_server.accept
2609+
binary_data.scan(/.{1,1024}/nm) do |s|
2610+
conn.print(s)
2611+
end
2612+
conn.shutdown(Socket::SHUT_WR)
2613+
conn.read
2614+
conn.close
2615+
data_server.close
2616+
sock.print("226 Transfer complete.\r\n")
2617+
}
2618+
begin
2619+
begin
2620+
ftp = Net::FTP.new
2621+
ftp.passive = true
2622+
ftp.use_pasv_ip = true
2623+
ftp.read_timeout *= 5 if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? # for --jit-wait
2624+
ftp.connect("127.0.0.1", server.port)
2625+
ftp.login
2626+
assert_match(/\AUSER /, commands.shift)
2627+
assert_match(/\APASS /, commands.shift)
2628+
assert_equal("TYPE I\r\n", commands.shift)
2629+
buf = ftp.getbinaryfile("foo", nil)
2630+
assert_equal(binary_data, buf)
2631+
assert_equal(Encoding::ASCII_8BIT, buf.encoding)
2632+
assert_equal("PASV\r\n", commands.shift)
2633+
assert_equal("RETR foo\r\n", commands.shift)
2634+
assert_equal(nil, commands.shift)
2635+
ensure
2636+
ftp.close if ftp
2637+
end
2638+
ensure
2639+
server.close
2640+
end
2641+
end
2642+
2643+
def test_use_pasv_invalid_ip
2644+
commands = []
2645+
binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3
2646+
server = create_ftp_server(nil, "127.0.0.1") { |sock|
2647+
sock.print("220 (test_ftp).\r\n")
2648+
commands.push(sock.gets)
2649+
sock.print("331 Please specify the password.\r\n")
2650+
commands.push(sock.gets)
2651+
sock.print("230 Login successful.\r\n")
2652+
commands.push(sock.gets)
2653+
sock.print("200 Switching to Binary mode.\r\n")
2654+
line = sock.gets
2655+
commands.push(line)
2656+
sock.print("227 Entering Passive Mode (999,0,0,1,48,57).\r\n")
2657+
commands.push(sock.gets)
2658+
}
2659+
begin
2660+
begin
2661+
ftp = Net::FTP.new
2662+
ftp.passive = true
2663+
ftp.use_pasv_ip = true
2664+
ftp.read_timeout *= 5 if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? # for --jit-wait
2665+
ftp.connect("127.0.0.1", server.port)
2666+
ftp.login
2667+
assert_match(/\AUSER /, commands.shift)
2668+
assert_match(/\APASS /, commands.shift)
2669+
assert_equal("TYPE I\r\n", commands.shift)
2670+
assert_raise(SocketError) do
2671+
ftp.getbinaryfile("foo", nil)
2672+
end
2673+
ensure
2674+
ftp.close if ftp
2675+
end
2676+
ensure
2677+
server.close
2678+
end
2679+
end
2680+
25282681
private
25292682

2530-
def create_ftp_server(sleep_time = nil)
2531-
server = TCPServer.new(SERVER_ADDR, 0)
2683+
def create_ftp_server(sleep_time = nil, addr = SERVER_ADDR)
2684+
server = TCPServer.new(addr, 0)
25322685
@thread = Thread.start do
25332686
if sleep_time
25342687
sleep(sleep_time)

0 commit comments

Comments
 (0)