Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions gems/net-imap/CVE-2025-25186.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
---
gem: net-imap
cve: 2025-25186
ghsa: 7fc5-f82f-cx69
url: https://github.com/ruby/net-imap/security/advisories/GHSA-7fc5-f82f-cx69
title: Possible DoS by memory exhaustion in net-imap
date: 2025-02-10
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?\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](https://ruby.github.io/net-imap/Net/IMAP/UIDPlusData.html),
[AppendUIDData](https://ruby.github.io/net-imap/Net/IMAP/AppendUIDData.html)
and [CopyUIDData](https://ruby.github.io/net-imap/Net/IMAP/CopyUIDData.html).

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 crash.
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
```
cvss_v3: 6.5
unaffected_versions:
- "< 0.3.2"
patched_versions:
- "~> 0.3.8"
- "~> 0.4.19"
- ">= 0.5.6"
related:
url:
- https://nvd.nist.gov/vuln/detail/CVE-2025-25186
- https://github.com/ruby/net-imap/security/advisories/GHSA-7fc5-f82f-cx69
- https://github.com/ruby/net-imap/commit/70e3ddd071a94e450b3238570af482c296380b35
- https://github.com/ruby/net-imap/commit/c8c5a643739d2669f0c9a6bb9770d0c045fd74a3
- https://github.com/ruby/net-imap/commit/cb92191b1ddce2d978d01b56a0883b6ecf0b1022
- https://github.com/advisories/GHSA-7fc5-f82f-cx69