{"answers":{"affected":"2 open source packages ship an affected version, including credsweeper.","exploited":"There is no known exploitation of GHSA-9mqm-qcwf-5qhg in the wild and no public exploit code.","fix":"Upgrade credsweeper to 1.16.0 or later.","what":"GHSA-9mqm-qcwf-5qhg is a denial of service vulnerability in credsweeper, published on 10 Jul 2026. An attacker can exploit it over the internet without logging in, with no action from the victim."},"fix_prompt":"Check this codebase for GHSA-9mqm-qcwf-5qhg and fix it if it is affected.\n\nWhat it is: GHSA-9mqm-qcwf-5qhg is a denial of service vulnerability in credsweeper, published on 10 Jul 2026. An attacker can exploit it over the internet without logging in, with no action from the victim.\nExploitation: There is no known exploitation of GHSA-9mqm-qcwf-5qhg in the wild and no public exploit code.\n\nAffected packages and fixed versions:\n- credsweeper (PyPI): affected from 1.4.9; fixed in 1.16.0\n- credsweeper (PyPI): affected from 1.4.9; fixed in 1.16.0\n\nTask:\n1. Search the dependency manifests and lockfiles (for example package.json, package-lock.json, requirements.txt, poetry.lock, uv.lock, pom.xml, build.gradle, go.mod, Cargo.lock, Gemfile.lock) and any Dockerfiles for the affected packages, including transitive dependencies.\n2. If an affected version is in use, upgrade it to the fixed version for its release line with the project's package manager, so the lockfile is regenerated. Prefer the smallest upgrade that fixes it.\n3. If there is no fix, or the upgrade would break the build, do not guess: explain the risk and the safest workaround instead.\n4. Run the tests and summarise what changed and why.\n5. If nothing in the project is affected, say so and change nothing.\n\nReference: https://insights.sbomify.com/GHSA-9mqm-qcwf-5qhg/ (Markdown: https://insights.sbomify.com/GHSA-9mqm-qcwf-5qhg/index.md)","id":"GHSA-9mqm-qcwf-5qhg","markdown":"https://insights.sbomify.com/GHSA-9mqm-qcwf-5qhg/index.md","name":"GHSA-9mqm-qcwf-5qhg","record":{"aliases":[],"analysis":{"component_type":"library","fix_type":"patch","impacts":["denial_of_service"],"network_reachable":"yes","non_default_config":true,"product_category":"Python credential scanner","requires_auth":"none","summary":"Attackers can cause resource exhaustion in CredSweeper by submitting crafted archives that bypass size limits during deep scanning.","user_interaction":"not_required","vuln_class":"denial_of_service"},"analysis_generated_by":"qwen3.8-flash-next","assigner":null,"cpe":[],"cvss":{"score":null,"severity":null,"source":null,"vector":null,"version":null},"cwes":[],"description":"### 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 \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'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 \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.creden","ecosystems":["PyPI"],"exploits":{"exploitdb":null,"first_seen":null,"github_poc":null,"max_poc_stars":null,"metasploit":null,"nuclei":null,"refs":[],"tier":null},"has_fix":true,"headline":"CredSweeper: Recursive archive size-limit bypass in deep scanner allows crafted compressed inputs to exhaust resources","id":"GHSA-9mqm-qcwf-5qhg","indexable":true,"kev":null,"kind":"advisory","links":{"ecosystem":"PyPI","product":null,"products":[],"type":"denial_of_service","year":"2026"},"n_packages":2,"n_references":null,"packages":[{"ecosystem":"PyPI","fixed":["1.16.0"],"introduced":["1.4.9"],"last_affected":[],"package":"credsweeper","purl":null,"source":"GHSA-9mqm-qcwf-5qhg"},{"ecosystem":"PyPI","fixed":["1.16.0"],"introduced":["1.4.9"],"last_affected":[],"package":"credsweeper","purl":"pkg:pypi/credsweeper","source":"GHSA-9mqm-qcwf-5qhg"}],"products":null,"published":"2026-07-10","related":[],"sources":[{"class":"attribution","licence":"CC BY 4.0","name":"GitHub Advisory Database","prefix":"GHSA"}],"ssvc":{"automatable":null,"exploitation":null,"technical_impact":null},"state":null,"updated":null,"vendors":null},"summary":"Attackers can cause resource exhaustion in CredSweeper by submitting crafted archives that bypass size limits during deep scanning.","url":"https://insights.sbomify.com/GHSA-9mqm-qcwf-5qhg/"}