GitHub Advisoryadvisory · PyPI
GHSA-9mqm-qcwf-5qhg CredSweeper: Recursive archive size-limit bypass in deep scanner allows crafted compressed inputs to exhaust resources
MEDIUM
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…