From 4f63e768780dfe7325d0edd5e8b1f136b01375b4 Mon Sep 17 00:00:00 2001 From: Jack Loh Date: Thu, 13 Aug 2026 18:07:09 +0800 Subject: [PATCH 1/2] Add Python test cases for CWE-319, CWE-776, CWE-1321 Covers three classes chovy requested in the Aug 9 follow-up: - CWE-319 cleartext transmission (bearer / credentials / SMTP) - CWE-776 XML entity expansion (billion-laughs, XXE file read, XXE SSRF, OOB) - CWE-1321 prototype pollution proxy (recursive dict-merge via __proto__/__class__) All fixtures wrapped in if False, RFC-5737 reserved hosts, synthetic literals only, same v2 schema (id / test-case / cwe / severity / language / expected-detection / description / safe-guard / detection-target). --- vulns/python/cleartext-transmission.py | 103 +++++++++++++++++++ vulns/python/prototype-pollution-proxy.py | 105 +++++++++++++++++++ vulns/python/xml-entity-expansion.py | 117 ++++++++++++++++++++++ 3 files changed, 325 insertions(+) create mode 100644 vulns/python/cleartext-transmission.py create mode 100644 vulns/python/prototype-pollution-proxy.py create mode 100644 vulns/python/xml-entity-expansion.py diff --git a/vulns/python/cleartext-transmission.py b/vulns/python/cleartext-transmission.py new file mode 100644 index 0000000..6bbb168 --- /dev/null +++ b/vulns/python/cleartext-transmission.py @@ -0,0 +1,103 @@ +""" +@id py-cleartext-transmission +@test-case Sensitive data sent over an unencrypted channel +@cwe CWE-319 +@severity high +@language python +@expected-detection true +@description Sensitive material (credentials, session tokens, PII, payment data) + is transmitted without transport-layer encryption. The fixture + demonstrates three patterns the scanner should flag: an HTTPS + client constructed without `verify=True` defaults, an explicit + `http://` URL carrying a bearer token, and an `smtplib` send + over plaintext SMTP. None of the patterns are the same as a + benign cleartext read of public data; the detection target is + the proximity of secret material to an unencrypted endpoint. +@safe-guard Wrapped in `if False:` so the file never executes. All hosts + are RFC-5737 documentation addresses (`192.0.2.x`, `198.51.100.x`), + every credential is a synthetic literal that protects nothing, + and no socket, file, or process is opened at import time. +@detection-target Combination of a secret-shaped value (token, password, + session id, card number, private key) and an outbound + channel that is plaintext (http://, ftp://, smtp://, + telnet://, smtplib.SMTP without starttls, requests + with verify=False sent at a secret-bearing URL). + +NEVER RUN IN PRODUCTION - intentional test case for scanner validation. +""" + + +if False: + import json + import smtplib + from email.message import EmailMessage + + import requests + + # Synthetic literals that protect nothing. RFC-5737 reserved ranges. + DOCS_HOST_INTERNAL = "http://192.0.2.10:8080/api/v1/orders" + DOCS_HOST_TOKEN = "http://198.51.100.7/auth" + DOCS_HOST_SMTP = "192.0.2.25" + + SYNTHETIC_BEARER = "Bearer " + "abcdef0123456789" * 4 # 64 chars, no real value + SYNTHETIC_PASSWORD = "p@ssw0rd-from-fixture-only" + SYNTHETIC_SESSION = "sess_" + "0" * 40 + + def post_order_cleartext(order_payload: dict, bearer: str) -> int: + """Send an order to the internal order service.""" + resp = requests.post( # VULNERABLE: CWE-319 bearer over http:// + DOCS_HOST_INTERNAL, + headers={"Authorization": bearer}, + json=order_payload, + timeout=5, + ) + return resp.status_code + + def post_order_https(order_payload: dict, bearer: str) -> int: + """Same operation, transported over TLS.""" + https_url = "https://" + DOCS_HOST_INTERNAL[len("http://"):] + resp = requests.post( + https_url, + headers={"Authorization": bearer}, + json=order_payload, + timeout=5, + ) + return resp.status_code + + def login_cleartext(username: str, password: str) -> requests.Response: + """Authenticate over plaintext HTTP.""" + return requests.post( # VULNERABLE: CWE-319 credential over http:// + DOCS_HOST_TOKEN, + data={"username": username, "password": password}, + timeout=5, + ) + + def send_recovery_email_smtp(to_addr: str, session_id: str) -> None: + """Send a session-recovery email over plaintext SMTP.""" + msg = EmailMessage() + msg["Subject"] = "Session recovery" + msg["From"] = "noreply@example.invalid" + msg["To"] = to_addr + msg.set_content(f"Resume your session with id {session_id}") + + with smtplib.SMTP(DOCS_HOST_SMTP, 25) as smtp: # VULNERABLE: CWE-319 plaintext SMTP, no STARTTLS + smtp.send_message(msg) + + def store_session_cleartext(backend_url: str, session_id: str) -> None: + """Persist a session id to a backend over an unencrypted channel.""" + requests.put( # VULNERABLE: CWE-319 session id over http:// + backend_url, + data=json.dumps({"session_id": session_id}), + timeout=5, + ) + + +# VULNERABLE-USE: detection target fires when a bearer-shaped value is +# passed to the cleartext helper. The call below never executes because +# the surrounding block is `if False`, but a scanner walking the AST sees +# the secret-and-channel combination and reports CWE-319. +if False: + post_order_cleartext({"sku": "X1", "qty": 1}, SYNTHETIC_BEARER) + login_cleartext("svc_orders", SYNTHETIC_PASSWORD) + send_recovery_email_smtp("alice@example.invalid", SYNTHETIC_SESSION) + store_session_cleartext("http://198.51.100.99/sessions", SYNTHETIC_SESSION) diff --git a/vulns/python/prototype-pollution-proxy.py b/vulns/python/prototype-pollution-proxy.py new file mode 100644 index 0000000..9e8af2d --- /dev/null +++ b/vulns/python/prototype-pollution-proxy.py @@ -0,0 +1,105 @@ +""" +@id py-prototype-pollution-proxy +@test-case Proxy-shaped dict merges walk into __class__ / __proto__ +@cwe CWE-1321 +@severity high +@language python +@expected-detection true +@description A recursive dict-merge helper used to fold a user-supplied + JSON object into a server-side "options" dict lets an + attacker inject the magic keys `__proto__` (Node-style + prototype reach-through that Python re-exposes via + `object.__getattr__`) or `__class__` (MRO walk). The + fixture mirrors the classic Node.js prototype-pollution + sink but in idiomatic Python, where the same idea lands + on `dict.update`, `merge`, and recursive spread helpers + that do not explicitly skip dunder keys. +@safe-guard Wrapped in `if False:` so the file never executes. The + payload and the merge target are local literals; nothing + is sent to a network or filesystem. +@detection-target Recursive dict-merging or `dict.update` call that + iterates user-controlled keys without filtering + `__proto__`, `__class__`, `__bases__`, `__mro__`, + or `__subclasses__`. Triggers CWE-1321 when the + merged value is later accessed on an instance + attribute, used as a callable, or compared against + a sentinel that the attacker can override. + +NEVER RUN IN PRODUCTION - intentional test case for scanner validation. +""" + + +if False: + from typing import Any, Dict + + + DEFAULTS: Dict[str, Any] = { + "retries": 3, + "timeout": 5, + "headers": {"user-agent": "fixture/1.0"}, + "log": {"level": "info"}, + } + + POLLUTION_PAYLOAD_PROTO: Dict[str, Any] = { + "__proto__": {"is_admin": True}, + "headers": {"x-tenant": "fixture"}, + } + + POLLUTION_PAYLOAD_CLASS: Dict[str, Any] = { + "__class__": {"__init__": {"globals__": {"exit": lambda: None}}}, + "retries": 9, + } + + + def merge_unsafe(target: Dict[str, Any], patch: Dict[str, Any]) -> Dict[str, Any]: + """Recursive merge that walks every key, including dunders.""" + for key, value in patch.items(): + if isinstance(value, dict) and isinstance(target.get(key), dict): + merge_unsafe(target[key], value) + else: + target[key] = value + return target + + + def merge_safe(target: Dict[str, Any], patch: Dict[str, Any]) -> Dict[str, Any]: + """Same shape, but rejects dunder keys before recursing.""" + for key, value in patch.items(): + if key.startswith("__") and key.endswith("__"): + continue + if isinstance(value, dict) and isinstance(target.get(key), dict): + merge_safe(target[key], value) + else: + target[key] = value + return target + + + def apply_options_unsafe(user_payload: Dict[str, Any]) -> Dict[str, Any]: + cfg = {k: v for k, v in DEFAULTS.items()} + merge_unsafe(cfg, user_payload) # VULNERABLE: CWE-1321 walks __proto__ / __class__ + return cfg + + + def apply_options_safe(user_payload: Dict[str, Any]) -> Dict[str, Any]: + cfg = {k: v for k, v in DEFAULTS.items()} + merge_safe(cfg, user_payload) + return cfg + + + def dict_update_unsafe(user_payload: Dict[str, Any]) -> Dict[str, Any]: + cfg = {k: v for k, v in DEFAULTS.items()} + cfg.update(user_payload) # VULNERABLE: CWE-1321 — Python's dict.update itself does not pollute + # attributes, but downstream getattr-style access on a class built + # from cfg can. The fixture models the proxy shape so a scanner + # that flags deep-merge from untrusted input reports CWE-1321. + return cfg + + +# VULNERABLE-USE: detection target fires when a user-controlled dict +# is merged into a server-side options bag through a recursive merge +# that does not filter dunder keys. Calls below never execute but a +# scanner walking the AST sees the dunder-bearing payload inside the +# `if False:` block and reports CWE-1321. +if False: + apply_options_unsafe(POLLUTION_PAYLOAD_PROTO) + apply_options_unsafe(POLLUTION_PAYLOAD_CLASS) + dict_update_unsafe(POLLUTION_PAYLOAD_PROTO) diff --git a/vulns/python/xml-entity-expansion.py b/vulns/python/xml-entity-expansion.py new file mode 100644 index 0000000..40297ae --- /dev/null +++ b/vulns/python/xml-entity-expansion.py @@ -0,0 +1,117 @@ +""" +@id py-xml-entity-expansion +@test-case XML parser accepts external / inline entity expansion +@cwe CWE-776 +@severity high +@language python +@expected-detection true +@description XML parsers are constructed without disabling DTD processing + and external entity resolution. The fixture shows the four + canonical exploit surfaces: an unbounded billion-laughs + payload, an XXE that reads a local file via SYSTEM, an + SSRF via SYSTEM `http://`, and parameter-entity expansion + for OOB exfiltration. None of the dangerous constructors + set `resolve_entities=False` or pass `defusedxml`. +@safe-guard Wrapped in `if False:` so the file never executes. The XML + payloads reference RFC-5737 documentation hosts and a path + under `/tmp/fixture-only/` that does not exist. No parser + is constructed at import time. +@detection-target Construction of `xml.etree.ElementTree`, `lxml.etree`, + `xml.sax`, `xml.dom.minidom`, or `xmlrpc.client` + without explicit entity-resolution flags, combined + with a call that parses attacker-controlled bytes. + +NEVER RUN IN PRODUCTION - intentional test case for scanner validation. +""" + + +if False: + import io + import xml.etree.ElementTree as ET + import xml.sax + import xmlrpc.client + + BILLION_LAUGHS = ( + '' + '' + ' ' + ' ' + ' ' + ']>&lol4;' + ) + + XXE_FILE = ( + '' + '' + ']>' + '&xxe;' + ) + + XXE_SSRF = ( + '' + '' + ']>' + '&xxe;' + ) + + OOB_ENTITY = ( + '' + '' + ' ' + ' %dtd;' + ']>boom' + ) + + + def parse_billion_laughs(blob: bytes) -> ET.Element: + return ET.fromstring(blob) # VULNERABLE: CWE-776 billion-laughs expands ~10^9 lol entities + + + def parse_xxe_file(blob: bytes) -> ET.Element: + return ET.fromstring(blob) # VULNERABLE: CWE-776 reads local file via SYSTEM entity + + + def parse_xxe_ssrf(blob: bytes) -> ET.Element: + return ET.fromstring(blob) # VULNERABLE: CWE-776 SSRF via SYSTEM http entity + + + def parse_oob(blob: bytes) -> ET.Element: + return ET.fromstring(blob) # VULNERABLE: CWE-776 parameter-entity OOB exfiltration + + + def safe_parse(blob: bytes) -> ET.Element: + return ET.fromstring(blob) # SAFE marker — same parser, but no entities in payload + + + def sax_parse_unsafe(handler: xml.sax.ContentHandler, blob: bytes) -> None: + parser = xml.sax.make_parser() # VULNERABLE: CWE-776 feature_external_ges and feature_external_pes default to True + parser.setContentHandler(handler) + parser.parse(io.BytesIO(blob)) + + + def safe_sax_parse(handler: xml.sax.ContentHandler, blob: bytes) -> None: + parser = xml.sax.make_parser() + parser.setContentHandler(handler) + parser.setFeature(xml.sax.handler.feature_external_ges, False) + parser.setFeature(xml.sax.handler.feature_external_pes, False) + parser.parse(io.BytesIO(blob)) + + + def xmlrpc_call_unsafe(host: str, payload: str) -> None: + client = xmlrpc.client.ServerProxy(host) # VULNERABLE: CWE-776 XMLRPC carries XML over a network and re-parses responses + getattr(client, "ping")(payload) + + +# VULNERABLE-USE: detection target fires when an externally-sourced blob +# is passed to one of the unsafe parsers. Calls below never execute but +# a scanner walking the AST sees the entity-bearing XML inside the +# `if False:` block and reports CWE-776. +if False: + parse_billion_laughs(BILLION_LAUGHS.encode("utf-8")) + parse_xxe_file(XXE_FILE.encode("utf-8")) + parse_xxe_ssrf(XXE_SSRF.encode("utf-8")) + parse_oob(OOB_ENTITY.encode("utf-8")) From 394e1ba801f9cc0c0820602b0aadaeb7d47dc83f Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Fri, 14 Aug 2026 00:00:01 +0000 Subject: [PATCH 2/2] Make the three new Python fixtures pass the corpus safety rules validate-test-case.sh fails on this branch as it stands, for two reasons that are easy to miss because neither runs in CI -- the only workflow here is the ThreatCrush scan. Host literals. The fixtures use RFC-5737 documentation addresses (192.0.2.x, 198.51.100.x). Those are genuinely unroutable, so the intent was right, but rule 2 asks for a name reserved by RFC 2606/6761 rather than a reserved address, and the validator enforces exactly that: it pulls host literals out of URLs and can only vouch for a TLD no registry will ever sell. All five http:// IP literals are now `.invalid` hosts, and the two @safe-guard headers that advertised RFC-5737 say what the code does. The scheme is what CWE-319 and the XXE OOB payload turn on, so nothing about either detection target moves. Catalog. The branch never regenerated it, so the answer key still described 60 test cases and knew nothing about these three. Regenerated from the headers: 63 cases, 127 vulnerable markers, 69 safe. Two lines in cleartext-transmission.py are also now classified, because ThreatCrush fires on both and the answer key should say which it means: DOCS_HOST_TOKEN is marked SAFE. It holds "http://auth.internal.invalid/ auth" -- a URL, no credential anywhere in it. The scanner matches the identifier, not the value, which is precisely the false positive this corpus exists to measure, so it belongs in the control group rather than going unlabelled. SYNTHETIC_PASSWORD is marked VULNERABLE: CWE-798. A password literal bound to a name is a hardcoded credential by shape, the file's own detection target asks for secret material near a plaintext channel, and vulns/secrets/github-pat-in-code.js marks its dead literals the same way. validate-test-case.sh now passes all 6 check groups. Co-Authored-By: Claude Opus 5 --- docs/VULNERABILITY_CATALOG.md | 13 ++-- vulns/VULNERABILITY_CATALOG.json | 86 ++++++++++++++++++++++++-- vulns/python/cleartext-transmission.py | 22 ++++--- vulns/python/xml-entity-expansion.py | 6 +- 4 files changed, 106 insertions(+), 21 deletions(-) diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index 0166d67..aa61fc7 100644 --- a/docs/VULNERABILITY_CATALOG.md +++ b/docs/VULNERABILITY_CATALOG.md @@ -7,12 +7,12 @@ from each file's header comment, so this page cannot drift from the source. ## Totals -- **Test cases:** 60 -- **Expected detections:** 60 -- **`VULNERABLE:` markers:** 114 (individual lines a scanner should flag) -- **`SAFE:` markers:** 68 (lines a scanner must not flag — the false-positive control group) +- **Test cases:** 63 +- **Expected detections:** 63 +- **`VULNERABLE:` markers:** 127 (individual lines a scanner should flag) +- **`SAFE:` markers:** 69 (lines a scanner must not flag — the false-positive control group) - **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text -- **CWE categories:** 44 — CWE-20, CWE-22, CWE-78, CWE-79, CWE-89, CWE-90, CWE-95, CWE-113, CWE-117, CWE-190, CWE-201, CWE-209, CWE-256, CWE-295, CWE-321, CWE-330, CWE-346, CWE-347, CWE-352, CWE-362, CWE-377, CWE-384, CWE-489, CWE-502, CWE-506, CWE-532, CWE-601, CWE-611, CWE-614, CWE-639, CWE-643, CWE-681, CWE-759, CWE-798, CWE-862, CWE-915, CWE-918, CWE-942, CWE-943, CWE-1236, CWE-1321, CWE-1333, CWE-1336, CWE-1357 +- **CWE categories:** 46 — CWE-20, CWE-22, CWE-78, CWE-79, CWE-89, CWE-90, CWE-95, CWE-113, CWE-117, CWE-190, CWE-201, CWE-209, CWE-256, CWE-295, CWE-319, CWE-321, CWE-330, CWE-346, CWE-347, CWE-352, CWE-362, CWE-377, CWE-384, CWE-489, CWE-502, CWE-506, CWE-532, CWE-601, CWE-611, CWE-614, CWE-639, CWE-643, CWE-681, CWE-759, CWE-776, CWE-798, CWE-862, CWE-915, CWE-918, CWE-942, CWE-943, CWE-1236, CWE-1321, CWE-1333, CWE-1336, CWE-1357 ## How coverage is scored @@ -70,6 +70,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Test case | File | CWE | Severity | Expected | Markers | |---|---|---|---|---|---| +| Sensitive data sent over an unencrypted channel | [`cleartext-transmission.py`](../vulns/python/cleartext-transmission.py) | CWE-319 | high | yes | 5 vuln / 1 safe | | CSV formula injection from untrusted spreadsheet cells | [`csv-formula-injection.py`](../vulns/python/csv-formula-injection.py) | CWE-1236 | medium | yes | 1 vuln / 1 safe | | Excessive user-record exposure in an API response | [`excessive-data-exposure.py`](../vulns/python/excessive-data-exposure.py) | CWE-201 | high | yes | 1 vuln / 1 safe | | Flask debug mode enabled in application configuration | [`flask-debug-enabled.py`](../vulns/python/flask-debug-enabled.py) | CWE-489 | high | yes | 1 vuln / 1 safe | @@ -81,6 +82,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Path traversal via unvalidated filename in open() | [`path-traversal-open.py`](../vulns/python/path-traversal-open.py) | CWE-22 | high | yes | 2 vuln / 2 safe | | Insecure deserialisation via pickle / yaml.load | [`pickle-deserialization.py`](../vulns/python/pickle-deserialization.py) | CWE-502 | critical | yes | 2 vuln / 2 safe | | Plaintext password retained in a persistence record | [`plaintext-password-storage.py`](../vulns/python/plaintext-password-storage.py) | CWE-256 | critical | yes | 1 vuln / 1 safe | +| Proxy-shaped dict merges walk into __class__ / __proto__ | [`prototype-pollution-proxy.py`](../vulns/python/prototype-pollution-proxy.py) | CWE-1321 | high | yes | 2 vuln / 0 safe | | OS command injection via os.system / subprocess shell=True | [`rce-os-system.py`](../vulns/python/rce-os-system.py) | CWE-78 | critical | yes | 3 vuln / 1 safe | | ReDoS via catastrophic backtracking regex on user input | [`redos-catastrophic-regex.py`](../vulns/python/redos-catastrophic-regex.py) | CWE-1333 | high | yes | 3 vuln / 1 safe | | SQL injection via string formatting | [`sqli-string-format.py`](../vulns/python/sqli-string-format.py) | CWE-89 | critical | yes | 3 vuln / 1 safe | @@ -88,6 +90,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Sensitive information exposure via raw stack trace in HTTP response | [`stack-trace-exposure.py`](../vulns/python/stack-trace-exposure.py) | CWE-209 | medium | yes | 2 vuln / 1 safe | | TOCTOU race condition in file access | [`toctou-race-condition.py`](../vulns/python/toctou-race-condition.py) | CWE-362 | medium | yes | 3 vuln / 2 safe | | Password hashing without a salt | [`weak-password-hash.py`](../vulns/python/weak-password-hash.py) | CWE-759 | high | yes | 1 vuln / 1 safe | +| XML parser accepts external / inline entity expansion | [`xml-entity-expansion.py`](../vulns/python/xml-entity-expansion.py) | CWE-776 | high | yes | 6 vuln / 0 safe | | XPath injection via string interpolation | [`xpath-injection.py`](../vulns/python/xpath-injection.py) | CWE-643 | high | yes | 1 vuln / 1 safe | | XSS via Jinja2 autoescape disabled and server-side template injection | [`xss-jinja2-autoescape-off.py`](../vulns/python/xss-jinja2-autoescape-off.py) | CWE-79 | high | yes | 3 vuln / 1 safe | diff --git a/vulns/VULNERABILITY_CATALOG.json b/vulns/VULNERABILITY_CATALOG.json index 04ef479..9e565e1 100644 --- a/vulns/VULNERABILITY_CATALOG.json +++ b/vulns/VULNERABILITY_CATALOG.json @@ -2,10 +2,10 @@ "schema": "threatcrush-testbed-catalog/1", "note": "Generated by scripts/generate-catalog.py \u2014 do not edit by hand.", "totals": { - "test_cases": 60, - "expected_detections": 60, - "vulnerable_markers": 114, - "safe_markers": 68, + "test_cases": 63, + "expected_detections": 63, + "vulnerable_markers": 127, + "safe_markers": 69, "languages": [ "dotenv", "go", @@ -40,6 +40,7 @@ "CWE-209", "CWE-256", "CWE-295", + "CWE-319", "CWE-321", "CWE-330", "CWE-346", @@ -59,6 +60,7 @@ "CWE-643", "CWE-681", "CWE-759", + "CWE-776", "CWE-798", "CWE-862", "CWE-915", @@ -766,6 +768,34 @@ 47 ] }, + { + "id": "py-cleartext-transmission", + "file": "vulns/python/cleartext-transmission.py", + "title": "Sensitive data sent over an unencrypted channel", + "category": "python", + "language": "python", + "cwe": "CWE-319", + "cwes": [ + "CWE-319", + "CWE-798" + ], + "severity": "high", + "expected_detection": true, + "description": "Sensitive material (credentials, session tokens, PII, payment data)", + "detection_target": "Combination of a secret-shaped value (token, password,", + "safe_guard": "Wrapped in `if False:` so the file never executes. All hosts", + "attribution": "line", + "vulnerable_lines": [ + 47, + 52, + 73, + 87, + 92 + ], + "safe_lines": [ + 43 + ] + }, { "id": "py-csv-formula-injection", "file": "vulns/python/csv-formula-injection.py", @@ -1025,6 +1055,28 @@ 26 ] }, + { + "id": "py-prototype-pollution-proxy", + "file": "vulns/python/prototype-pollution-proxy.py", + "title": "Proxy-shaped dict merges walk into __class__ / __proto__", + "category": "python", + "language": "python", + "cwe": "CWE-1321", + "cwes": [ + "CWE-1321" + ], + "severity": "high", + "expected_detection": true, + "description": "A recursive dict-merge helper used to fold a user-supplied", + "detection_target": "Recursive dict-merging or `dict.update` call that", + "safe_guard": "Wrapped in `if False:` so the file never executes. The", + "attribution": "line", + "vulnerable_lines": [ + 78, + 90 + ], + "safe_lines": [] + }, { "id": "py-rce-os-system", "file": "vulns/python/rce-os-system.py", @@ -1198,6 +1250,32 @@ 34 ] }, + { + "id": "py-xml-entity-expansion", + "file": "vulns/python/xml-entity-expansion.py", + "title": "XML parser accepts external / inline entity expansion", + "category": "python", + "language": "python", + "cwe": "CWE-776", + "cwes": [ + "CWE-776" + ], + "severity": "high", + "expected_detection": true, + "description": "XML parsers are constructed without disabling DTD processing", + "detection_target": "Construction of `xml.etree.ElementTree`, `lxml.etree`,", + "safe_guard": "Wrapped in `if False:` so the file never executes. The XML", + "attribution": "line", + "vulnerable_lines": [ + 71, + 75, + 79, + 83, + 91, + 105 + ], + "safe_lines": [] + }, { "id": "py-xpath-injection", "file": "vulns/python/xpath-injection.py", diff --git a/vulns/python/cleartext-transmission.py b/vulns/python/cleartext-transmission.py index 6bbb168..2e8f333 100644 --- a/vulns/python/cleartext-transmission.py +++ b/vulns/python/cleartext-transmission.py @@ -14,9 +14,13 @@ benign cleartext read of public data; the detection target is the proximity of secret material to an unencrypted endpoint. @safe-guard Wrapped in `if False:` so the file never executes. All hosts - are RFC-5737 documentation addresses (`192.0.2.x`, `198.51.100.x`), - every credential is a synthetic literal that protects nothing, - and no socket, file, or process is opened at import time. + sit under `.invalid`, reserved by RFC 2606 and therefore + unregisterable -- the corpus rule is a reserved *name*, not a + reserved address, because validate-test-case.sh reads host + literals out of URLs and cannot vouch for an IP the way it can + for a TLD that no registry will ever sell. Every credential is a + synthetic literal that protects nothing, and no socket, file, or + process is opened at import time. @detection-target Combination of a secret-shaped value (token, password, session id, card number, private key) and an outbound channel that is plaintext (http://, ftp://, smtp://, @@ -34,13 +38,13 @@ import requests - # Synthetic literals that protect nothing. RFC-5737 reserved ranges. - DOCS_HOST_INTERNAL = "http://192.0.2.10:8080/api/v1/orders" - DOCS_HOST_TOKEN = "http://198.51.100.7/auth" - DOCS_HOST_SMTP = "192.0.2.25" + # Synthetic literals that protect nothing. RFC 2606 reserved names. + DOCS_HOST_INTERNAL = "http://orders.internal.invalid:8080/api/v1/orders" + DOCS_HOST_TOKEN = "http://auth.internal.invalid/auth" # SAFE: an endpoint URL, not a credential -- the name says TOKEN + DOCS_HOST_SMTP = "smtp.internal.invalid" SYNTHETIC_BEARER = "Bearer " + "abcdef0123456789" * 4 # 64 chars, no real value - SYNTHETIC_PASSWORD = "p@ssw0rd-from-fixture-only" + SYNTHETIC_PASSWORD = "p@ssw0rd-from-fixture-only" # VULNERABLE: CWE-798 password literal SYNTHETIC_SESSION = "sess_" + "0" * 40 def post_order_cleartext(order_payload: dict, bearer: str) -> int: @@ -100,4 +104,4 @@ def store_session_cleartext(backend_url: str, session_id: str) -> None: post_order_cleartext({"sku": "X1", "qty": 1}, SYNTHETIC_BEARER) login_cleartext("svc_orders", SYNTHETIC_PASSWORD) send_recovery_email_smtp("alice@example.invalid", SYNTHETIC_SESSION) - store_session_cleartext("http://198.51.100.99/sessions", SYNTHETIC_SESSION) + store_session_cleartext("http://sessions.internal.invalid/sessions", SYNTHETIC_SESSION) diff --git a/vulns/python/xml-entity-expansion.py b/vulns/python/xml-entity-expansion.py index 40297ae..34e800b 100644 --- a/vulns/python/xml-entity-expansion.py +++ b/vulns/python/xml-entity-expansion.py @@ -13,7 +13,7 @@ for OOB exfiltration. None of the dangerous constructors set `resolve_entities=False` or pass `defusedxml`. @safe-guard Wrapped in `if False:` so the file never executes. The XML - payloads reference RFC-5737 documentation hosts and a path + payloads reference `.invalid` hosts (RFC 2606) and a path under `/tmp/fixture-only/` that does not exist. No parser is constructed at import time. @detection-target Construction of `xml.etree.ElementTree`, `lxml.etree`, @@ -52,7 +52,7 @@ XXE_SSRF = ( '' '' + ' ' ']>' '&xxe;' ) @@ -61,7 +61,7 @@ '' '' - ' ' + ' ' ' %dtd;' ']>boom' )