diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index 33039de..a23a762 100644 --- a/docs/VULNERABILITY_CATALOG.md +++ b/docs/VULNERABILITY_CATALOG.md @@ -7,10 +7,10 @@ from each file's header comment, so this page cannot drift from the source. ## Totals -- **Test cases:** 116 -- **Expected detections:** 116 -- **`VULNERABLE:` markers:** 215 (individual lines a scanner should flag) -- **`SAFE:` markers:** 131 (lines a scanner must not flag — the false-positive control group) +- **Test cases:** 117 +- **Expected detections:** 117 +- **`VULNERABLE:` markers:** 217 (individual lines a scanner should flag) +- **`SAFE:` markers:** 132 (lines a scanner must not flag — the false-positive control group) - **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text - **CWE categories:** 84 — CWE-20, CWE-22, CWE-78, CWE-79, CWE-89, CWE-90, CWE-94, CWE-95, CWE-113, CWE-117, CWE-129, CWE-190, CWE-201, CWE-203, CWE-208, CWE-209, CWE-256, CWE-287, CWE-288, CWE-291, CWE-295, CWE-306, CWE-307, CWE-319, CWE-321, CWE-326, CWE-327, CWE-329, CWE-330, CWE-338, CWE-345, CWE-346, CWE-347, CWE-352, CWE-362, CWE-377, CWE-384, CWE-400, CWE-434, CWE-441, CWE-460, CWE-472, CWE-475, CWE-480, CWE-488, CWE-489, CWE-502, CWE-506, CWE-509, CWE-512, CWE-521, CWE-525, CWE-532, CWE-598, CWE-601, CWE-602, CWE-611, CWE-613, CWE-614, CWE-620, CWE-639, CWE-640, CWE-643, CWE-681, CWE-693, CWE-732, CWE-759, CWE-776, CWE-798, CWE-835, CWE-862, CWE-863, CWE-915, CWE-916, CWE-918, CWE-922, CWE-942, CWE-943, CWE-1021, CWE-1236, CWE-1321, CWE-1333, CWE-1336, CWE-1357 @@ -125,6 +125,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Open redirect via unvalidated next parameter | [`open-redirect.py`](../vulns/python/open-redirect.py) | CWE-601 | medium | yes | 2 vuln / 1 safe | | 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 | | PBKDF2 password hashing with an insufficient iteration count | [`pbkdf2-low-iteration-count.py`](../vulns/python/pbkdf2-low-iteration-count.py) | CWE-916 | high | yes | 1 vuln / 1 safe | +| Permissive cross-origin policy with credentialed requests | [`permissive-cors-credentials.py`](../vulns/python/permissive-cors-credentials.py) | CWE-942 | medium | yes | 2 vuln / 1 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 | diff --git a/vulns/VULNERABILITY_CATALOG.json b/vulns/VULNERABILITY_CATALOG.json index 6f8dbe5..a29999a 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": 116, - "expected_detections": 116, - "vulnerable_markers": 215, - "safe_markers": 131, + "test_cases": 117, + "expected_detections": 117, + "vulnerable_markers": 217, + "safe_markers": 132, "languages": [ "dotenv", "go", @@ -2132,6 +2132,30 @@ 35 ] }, + { + "id": "py-permissive-cors-credentials", + "file": "vulns/python/permissive-cors-credentials.py", + "title": "Permissive cross-origin policy with credentialed requests", + "category": "python", + "language": "python", + "cwe": "CWE-942", + "cwes": [ + "CWE-942" + ], + "severity": "medium", + "expected_detection": true, + "description": "The API accepts cookie-authenticated requests from any origin:", + "detection_target": "Flask/FastAPI response header writes where", + "safe_guard": "Both handlers are wrapped in `if False:` (unreachable dead code).", + "attribution": "line", + "vulnerable_lines": [ + 30, + 40 + ], + "safe_lines": [ + 55 + ] + }, { "id": "py-pickle-deserialization", "file": "vulns/python/pickle-deserialization.py", diff --git a/vulns/python/permissive-cors-credentials.py b/vulns/python/permissive-cors-credentials.py new file mode 100644 index 0000000..62e795d --- /dev/null +++ b/vulns/python/permissive-cors-credentials.py @@ -0,0 +1,59 @@ +""" +@id py-permissive-cors-credentials +@test-case Permissive cross-origin policy with credentialed requests +@cwe CWE-942 +@severity medium +@language python +@expected-detection true +@description The API accepts cookie-authenticated requests from any origin: + Access-Control-Allow-Origin is echoed from the request (or set to + `*`) while credentials are allowed. Any site the victim visits can + then issue authenticated reads and writes, since the browser will + attach the session cookies. The safe counterpart allows only a + fixed allow-listed origin. Detection target is the pairing of a + wildcard/reflected ACAO with allow-credentials, not the string + alone. +@safe-guard Both handlers are wrapped in `if False:` (unreachable dead code). + Nothing is served over the network and no origin is contacted; the + allow-list origin is an unresolvable `.invalid` name. +@detection-target Flask/FastAPI response header writes where + Access-Control-Allow-Origin is `*` or request-reflected while + Access-Control-Allow-Credentials is true on the same path. + +NEVER RUN IN PRODUCTION - intentional test case for scanner validation. +""" + + +def account_json_response(after_request, request_origin): + if False: + resp = after_request() + # VULNERABLE: CWE-942 - echoes any origin + allows cookies + resp.headers["Access-Control-Allow-Origin"] = request_origin + resp.headers["Access-Control-Allow-Credentials"] = "true" + return resp + return None + + +def account_json_response_wildcard(after_request): + if False: + resp = after_request() + # VULNERABLE: CWE-942 - wildcard origin with credentials is rejected by + # browsers, but the code (and some proxies) treat it as permissive CORS + resp.headers["Access-Control-Allow-Origin"] = "*" + resp.headers["Access-Control-Allow-Credentials"] = "true" + return resp + return None + + +def account_json_response_safe(after_request): + """Safe counterpart - the scanner should NOT flag this. + + @expected-detection false + """ + if False: + resp = after_request() + # SAFE: fixed allow-listed origin, credentials allowed only for it + resp.headers["Access-Control-Allow-Origin"] = "https://app.internal.invalid" + resp.headers["Access-Control-Allow-Credentials"] = "true" + return resp + return None \ No newline at end of file