GitHub Advisoryadvisory · PyPI

GHSA-9mqm-qcwf-5qhg CredSweeper: Recursive archive size-limit bypass in deep scanner allows crafted compressed inputs to exhaust resources

MEDIUM Published 2026-07-10 Modified 2026-07-10 Official page ↗
Ecosystem
PyPI
Packages
credsweeper
Severity
Moderate
CVSS vector
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
Weaknesses
CWE-400, CWE-409
Reviewed
2026-07-10

Affected packages

• PyPI credsweeper: >= 1.4.9, < 1.16.0 → fixed in 1.16.0

Description

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` ```python 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 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` ```python 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. 4. 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: ```bash git clone https://github.com/Samsung/CredSweeper.git cd CredSweeper git checkout 8b081acf04311eafe8fbd66ea41d02b0a7a4c6f6 ``` 2. Save the following as `proof_poc.py` one directory above the repository, or adjust `REPO_ROOT` accordingly: ```python 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.candid…

Related

Weakness · CWE (2)

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

References