|
| 1 | +--- |
| 2 | +layout: advisory |
| 3 | +title: 'CVE-2025-25186 (net-imap): Possible DoS by memory exhaustion in net-imap' |
| 4 | +comments: false |
| 5 | +categories: |
| 6 | +- net-imap |
| 7 | +advisory: |
| 8 | + gem: net-imap |
| 9 | + cve: 2025-25186 |
| 10 | + ghsa: 7fc5-f82f-cx69 |
| 11 | + url: https://github.com/ruby/net-imap/security/advisories/GHSA-7fc5-f82f-cx69 |
| 12 | + title: Possible DoS by memory exhaustion in net-imap |
| 13 | + date: 2025-02-10 |
| 14 | + description: | |
| 15 | + ### Summary |
| 16 | +
|
| 17 | + There is a possibility for denial of service by memory exhaustion in |
| 18 | + `net-imap`'s response parser. At any time while the client is |
| 19 | + connected, a malicious server can send can send highly compressed |
| 20 | + `uid-set` data which is automatically read by the client's receiver |
| 21 | + thread. The response parser uses `Range#to_a` to convert the |
| 22 | + `uid-set` data into arrays of integers, with no limitation on the |
| 23 | + expanded size of the ranges. |
| 24 | +
|
| 25 | + ### Details |
| 26 | +
|
| 27 | + IMAP's `uid-set` and `sequence-set` formats can compress ranges of |
| 28 | + numbers, for example: `"1,2,3,4,5"` and `"1:5"` both represent the |
| 29 | + same set. When `Net::IMAP::ResponseParser` receives `APPENDUID` or |
| 30 | + `COPYUID` response codes, it expands each `uid-set` into an array of |
| 31 | + integers. On a 64 bit system, these arrays will expand to 8 bytes |
| 32 | + for each number in the set. A malicious IMAP server may send |
| 33 | + specially crafted `APPENDUID` or `COPYUID` responses with very large |
| 34 | + `uid-set` ranges. |
| 35 | +
|
| 36 | + The `Net::IMAP` client parses each server response in a separate |
| 37 | + thread, as soon as each responses is received from the server. |
| 38 | + This attack works even when the client does not handle the |
| 39 | + `APPENDUID` or `COPYUID` responses. |
| 40 | +
|
| 41 | + Malicious inputs: |
| 42 | +
|
| 43 | + ```ruby |
| 44 | + # 40 bytes expands to ~1.6GB: |
| 45 | + "* OK [COPYUID 1 1:99999999 1:99999999]\r\n" |
| 46 | +
|
| 47 | + # Worst *valid* input scenario (using uint32 max), |
| 48 | + # 44 bytes expands to 64GiB: |
| 49 | + "* OK [COPYUID 1 1:4294967295 1:4294967295]\r\n" |
| 50 | +
|
| 51 | + # Numbers must be non-zero uint32, but this isn't validated. Arrays |
| 52 | + # larger than UINT32_MAX can be created. For example, the following |
| 53 | + # would theoretically expand to almost 800 exabytes: |
| 54 | + "* OK [COPYUID 1 1:99999999999999999999 1:99999999999999999999]\r\n" |
| 55 | + ``` |
| 56 | +
|
| 57 | + Simple way to test this: |
| 58 | + ```ruby |
| 59 | + require "net/imap" |
| 60 | +
|
| 61 | + def test(size) |
| 62 | + input = "A004 OK [COPYUID 1 1:#{size} 1:#{size}] too large?\n" |
| 63 | + parser = Net::IMAP::ResponseParser.new |
| 64 | + parser.parse input |
| 65 | + end |
| 66 | +
|
| 67 | + test(99_999_999) |
| 68 | + ``` |
| 69 | +
|
| 70 | + ### Fixes |
| 71 | +
|
| 72 | + #### Preferred Fix, minor API changes |
| 73 | +
|
| 74 | + Upgrade to v0.4.19, v0.5.6, or higher, and configure: |
| 75 | +
|
| 76 | + ```ruby |
| 77 | + # globally |
| 78 | + Net::IMAP.config.parser_use_deprecated_uidplus_data = false |
| 79 | + # per-client |
| 80 | + imap = Net::IMAP.new(hostname, ssl: true, |
| 81 | + parser_use_deprecated_uidplus_data: false) |
| 82 | + imap.config.parser_use_deprecated_uidplus_data = false |
| 83 | + ``` |
| 84 | +
|
| 85 | + This replaces `UIDPlusData` with `AppendUIDData` and `CopyUIDData`. |
| 86 | + These classes store their UIDs as `Net::IMAP::SequenceSet` objects |
| 87 | + (_not_ expanded into arrays of integers). Code that does not handle |
| 88 | + `APPENDUID` or `COPYUID` responses will not notice any difference. |
| 89 | + Code that does handle these responses _may_ need to be updated. See |
| 90 | + the documentation for |
| 91 | + [UIDPlusData](https://ruby.github.io/net-imap/Net/IMAP/UIDPlusData.html), |
| 92 | + [AppendUIDData](https://ruby.github.io/net-imap/Net/IMAP/AppendUIDData.html) |
| 93 | + and [CopyUIDData](https://ruby.github.io/net-imap/Net/IMAP/CopyUIDData.html). |
| 94 | +
|
| 95 | + For v0.3.8, this option is not available. |
| 96 | + For v0.4.19, the default value is `true`. |
| 97 | + For v0.5.6, the default value is `:up_to_max_size`. |
| 98 | + For v0.6.0, the only allowed value will be `false` _(`UIDPlusData` |
| 99 | + will be removed from v0.6)_. |
| 100 | +
|
| 101 | + #### Mitigation, backward compatible API |
| 102 | +
|
| 103 | + Upgrade to v0.3.8, v0.4.19, v0.5.6, or higher. |
| 104 | +
|
| 105 | + For backward compatibility, `uid-set` can still be expanded |
| 106 | + into an array, but a maximum limit will be applied. |
| 107 | +
|
| 108 | + Assign `config.parser_max_deprecated_uidplus_data_size` to set the |
| 109 | + maximum `UIDPlusData` UID set size. When |
| 110 | + `config.parser_use_deprecated_uidplus_data == true`, larger sets will crash. |
| 111 | + When `config.parser_use_deprecated_uidplus_data == :up_to_max_size`, |
| 112 | + larger sets will use `AppendUIDData` or `CopyUIDData`. |
| 113 | +
|
| 114 | + For v0.3,8, this limit is _hard-coded_ to 10,000, and larger sets |
| 115 | + will always raise `Net::IMAP::ResponseParseError`. |
| 116 | + For v0.4.19, the limit defaults to 1000. |
| 117 | + For v0.5.6, the limit defaults to 100. |
| 118 | + For v0.6.0, the limit will be ignored _(`UIDPlusData` will be |
| 119 | + removed from v0.6)_. |
| 120 | +
|
| 121 | + #### Please Note: unhandled responses |
| 122 | +
|
| 123 | + If the client does not add response handlers to prune unhandled |
| 124 | + responses, a malicious server can still eventually exhaust all |
| 125 | +
|
| 126 | + client memory, by repeatedly sending malicious responses. However, |
| 127 | + `net-imap` has always retained unhandled responses, and it has always |
| 128 | + been necessary for long-lived connections to prune these responses. |
| 129 | + _This is not significantly different from connecting to a trusted |
| 130 | + server with a long-lived connection._ To limit the maximum number |
| 131 | + of retained responses, a simple handler might look something like |
| 132 | + the following: |
| 133 | +
|
| 134 | + ```ruby |
| 135 | + limit = 1000 |
| 136 | + imap.add_response_handler do |resp| |
| 137 | + next unless resp.respond_to?(:name) && resp.respond_to?(:data) |
| 138 | + name = resp.name |
| 139 | + code = resp.data.code&.name if resp.data.respond_to?(:code) |
| 140 | + if Net::IMAP::VERSION > "0.4.0" |
| 141 | + imap.responses(name) { _1.slice!(0...-limit) } |
| 142 | + imap.responses(code) { _1.slice!(0...-limit) } |
| 143 | + else |
| 144 | + imap.responses(name).slice!(0...-limit) |
| 145 | + imap.responses(code).slice!(0...-limit) |
| 146 | + end |
| 147 | + end |
| 148 | + ``` |
| 149 | + cvss_v3: 6.5 |
| 150 | + unaffected_versions: |
| 151 | + - "< 0.3.2" |
| 152 | + patched_versions: |
| 153 | + - "~> 0.3.8" |
| 154 | + - "~> 0.4.19" |
| 155 | + - ">= 0.5.6" |
| 156 | + related: |
| 157 | + url: |
| 158 | + - https://nvd.nist.gov/vuln/detail/CVE-2025-25186 |
| 159 | + - https://github.com/ruby/net-imap/security/advisories/GHSA-7fc5-f82f-cx69 |
| 160 | + - https://github.com/ruby/net-imap/commit/70e3ddd071a94e450b3238570af482c296380b35 |
| 161 | + - https://github.com/ruby/net-imap/commit/c8c5a643739d2669f0c9a6bb9770d0c045fd74a3 |
| 162 | + - https://github.com/ruby/net-imap/commit/cb92191b1ddce2d978d01b56a0883b6ecf0b1022 |
| 163 | + - https://github.com/advisories/GHSA-7fc5-f82f-cx69 |
| 164 | +--- |
0 commit comments