diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index 98aa751..1b592e6 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:** 39 -- **Expected detections:** 39 -- **`VULNERABLE:` markers:** 93 (individual lines a scanner should flag) -- **`SAFE:` markers:** 47 (lines a scanner must not flag — the false-positive control group) +- **Test cases:** 59 +- **Expected detections:** 59 +- **`VULNERABLE:` markers:** 113 (individual lines a scanner should flag) +- **`SAFE:` markers:** 67 (lines a scanner must not flag — the false-positive control group) - **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text -- **CWE categories:** 24 — CWE-22, CWE-78, CWE-79, CWE-89, CWE-95, CWE-190, CWE-209, CWE-347, CWE-352, CWE-362, CWE-377, CWE-502, CWE-506, CWE-532, CWE-601, CWE-611, CWE-639, CWE-681, CWE-798, CWE-918, CWE-1321, CWE-1333, CWE-1336, CWE-1357 +- **CWE categories:** 43 — 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-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 ## How coverage is scored @@ -44,14 +44,24 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Test case | File | CWE | Severity | Expected | Markers | |---|---|---|---|---|---| +| Sensitive session cookie without the Secure attribute | [`cookie-security-flags.js`](../vulns/javascript/cookie-security-flags.js) | CWE-614 | medium | yes | 1 vuln / 1 safe | +| Credentialed CORS configured with a wildcard origin | [`cors-wildcard-credentials.js`](../vulns/javascript/cors-wildcard-credentials.js) | CWE-942 | high | yes | 1 vuln / 1 safe | | CSRF via missing anti-CSRF token on state-changing POST | [`csrf-missing-token.js`](../vulns/javascript/csrf-missing-token.js) | CWE-352 | high | yes | 3 vuln / 1 safe | +| Hardcoded cryptographic key used for application encryption | [`hardcoded-crypto-key.js`](../vulns/javascript/hardcoded-crypto-key.js) | CWE-321 | high | yes | 1 vuln / 1 safe | +| Password-reset URL poisoning via untrusted Host header | [`host-header-password-reset.js`](../vulns/javascript/host-header-password-reset.js) | CWE-346 | high | yes | 1 vuln / 1 safe | +| HTTP response splitting via unsanitized header value | [`http-header-injection.js`](../vulns/javascript/http-header-injection.js) | CWE-113 | high | yes | 1 vuln / 1 safe | | IDOR via unscoped object lookup | [`idor-unscoped-object-lookup.js`](../vulns/javascript/idor-unscoped-object-lookup.js) | CWE-639 | high | yes | 1 vuln / 1 safe | +| Predictable security token generated with Math.random | [`insecure-random-token.js`](../vulns/javascript/insecure-random-token.js) | CWE-330 | high | yes | 1 vuln / 1 safe | | JWT signature validation bypass via decode-only parsing | [`jwt-decode-without-verify.js`](../vulns/javascript/jwt-decode-without-verify.js) | CWE-347 | high | yes | 1 vuln / 1 safe | +| Log injection via untrusted request metadata | [`log-injection.js`](../vulns/javascript/log-injection.js) | CWE-117 | medium | yes | 1 vuln / 1 safe | +| Mass assignment through Object.assign with request data | [`mass-assignment-object-assign.js`](../vulns/javascript/mass-assignment-object-assign.js) | CWE-915 | high | yes | 1 vuln / 1 safe | +| MongoDB NoSQL injection via attacker-controlled query object | [`nosql-injection-mongodb.js`](../vulns/javascript/nosql-injection-mongodb.js) | CWE-943 | high | yes | 1 vuln / 1 safe | | Open redirect via unvalidated next parameter | [`open-redirect.js`](../vulns/javascript/open-redirect.js) | CWE-601 | medium | yes | 3 vuln / 1 safe | | Prototype pollution via recursive merge | [`prototype-pollution.js`](../vulns/javascript/prototype-pollution.js) | CWE-1321 | high | yes | 2 vuln / 1 safe | | OS command injection via child_process.exec | [`rce-child-process.js`](../vulns/javascript/rce-child-process.js) | CWE-78 | critical | yes | 2 vuln / 1 safe | | SQL injection via string concatenation | [`sqli-raw-concat.js`](../vulns/javascript/sqli-raw-concat.js) | CWE-89 | critical | yes | 2 vuln / 1 safe | | Server-side request forgery via user-supplied URL | [`ssrf-request-user-url.js`](../vulns/javascript/ssrf-request-user-url.js) | CWE-918 | high | yes | 2 vuln / 2 safe | +| TLS certificate validation disabled on an HTTPS agent | [`tls-reject-unauthorized-false.js`](../vulns/javascript/tls-reject-unauthorized-false.js) | CWE-295 | high | yes | 1 vuln / 1 safe | | Reflected XSS via innerHTML | [`xss-dom-innerHTML.js`](../vulns/javascript/xss-dom-innerHTML.js) | CWE-79 | high | yes | 2 vuln / 1 safe | | Code injection via eval() of user input | [`xss-eval-user-input.js`](../vulns/javascript/xss-eval-user-input.js) | CWE-95 | critical | yes | 3 vuln / 1 safe | @@ -59,15 +69,25 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Test case | File | CWE | Severity | Expected | Markers | |---|---|---|---|---|---| +| 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 | +| Hardcoded secret used to configure session signing | [`hardcoded-session-secret.py`](../vulns/python/hardcoded-session-secret.py) | CWE-798 | high | yes | 1 vuln / 1 safe | | Insecure temp file creation with predictable path | [`insecure-temp-file.py`](../vulns/python/insecure-temp-file.py) | CWE-377 | medium | yes | 3 vuln / 1 safe | +| LDAP injection via unescaped search filter | [`ldap-injection.py`](../vulns/python/ldap-injection.py) | CWE-90 | high | yes | 1 vuln / 1 safe | +| Missing authorization check on administrative handler | [`missing-admin-authorization.py`](../vulns/python/missing-admin-authorization.py) | CWE-862 | high | yes | 1 vuln / 1 safe | +| Missing range validation for a user-supplied price | [`negative-price-validation.py`](../vulns/python/negative-price-validation.py) | CWE-20 | medium | yes | 1 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 | | 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 | | 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 | | Server-side request forgery via requests.get on user-supplied URL | [`ssrf-requests-user-url.py`](../vulns/python/ssrf-requests-user-url.py) | CWE-918 | high | yes | 2 vuln / 1 safe | | 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 | +| 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 | ## Ruby diff --git a/vulns/VULNERABILITY_CATALOG.json b/vulns/VULNERABILITY_CATALOG.json index 1557680..1120d56 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": 39, - "expected_detections": 39, - "vulnerable_markers": 93, - "safe_markers": 47, + "test_cases": 59, + "expected_detections": 59, + "vulnerable_markers": 113, + "safe_markers": 67, "languages": [ "dotenv", "go", @@ -26,26 +26,45 @@ "supply-chain" ], "cwes": [ + "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-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", @@ -251,6 +270,52 @@ 46 ] }, + { + "id": "js-cookie-security-flags", + "file": "vulns/javascript/cookie-security-flags.js", + "title": "Sensitive session cookie without the Secure attribute", + "category": "javascript", + "language": "javascript", + "cwe": "CWE-614", + "cwes": [ + "CWE-614" + ], + "severity": "medium", + "expected_detection": true, + "description": "A session identifier is placed in a cookie that explicitly", + "detection_target": "Session cookies configured with secure:false or without", + "safe_guard": "Both handlers are unreachable behind if (false), and the response", + "attribution": "line", + "vulnerable_lines": [ + 20 + ], + "safe_lines": [ + 31 + ] + }, + { + "id": "js-cors-wildcard-credentials", + "file": "vulns/javascript/cors-wildcard-credentials.js", + "title": "Credentialed CORS configured with a wildcard origin", + "category": "javascript", + "language": "javascript", + "cwe": "CWE-942", + "cwes": [ + "CWE-942" + ], + "severity": "high", + "expected_detection": true, + "description": "A CORS policy combines a wildcard allowed origin with credential", + "detection_target": "CORS configuration that combines origin '*' with credentials", + "safe_guard": "The entire fixture is wrapped in if (false), so no middleware or", + "attribution": "line", + "vulnerable_lines": [ + 20 + ], + "safe_lines": [ + 30 + ] + }, { "id": "js-csrf-missing-token", "file": "vulns/javascript/csrf-missing-token.js", @@ -276,6 +341,75 @@ 55 ] }, + { + "id": "js-hardcoded-crypto-key", + "file": "vulns/javascript/hardcoded-crypto-key.js", + "title": "Hardcoded cryptographic key used for application encryption", + "category": "javascript", + "language": "javascript", + "cwe": "CWE-321", + "cwes": [ + "CWE-321" + ], + "severity": "high", + "expected_detection": true, + "description": "An obviously synthetic AES key is embedded in source and passed", + "detection_target": "Fixed key literals reaching cryptographic key or cipher", + "safe_guard": "The key is a repeating test-only placeholder, both examples are", + "attribution": "line", + "vulnerable_lines": [ + 22 + ], + "safe_lines": [ + 30 + ] + }, + { + "id": "js-host-header-password-reset", + "file": "vulns/javascript/host-header-password-reset.js", + "title": "Password-reset URL poisoning via untrusted Host header", + "category": "javascript", + "language": "javascript", + "cwe": "CWE-346", + "cwes": [ + "CWE-346" + ], + "severity": "high", + "expected_detection": true, + "description": "A password-reset URL uses the request Host header as its trust", + "detection_target": "Host or X-Forwarded-Host data used to construct a security-", + "safe_guard": "URL construction is behind if (false); no email, network request,", + "attribution": "line", + "vulnerable_lines": [ + 20 + ], + "safe_lines": [ + 27 + ] + }, + { + "id": "js-http-header-injection", + "file": "vulns/javascript/http-header-injection.js", + "title": "HTTP response splitting via unsanitized header value", + "category": "javascript", + "language": "javascript", + "cwe": "CWE-113", + "cwes": [ + "CWE-113" + ], + "severity": "high", + "expected_detection": true, + "description": "A query parameter is copied directly into a response header, so", + "detection_target": "Request data reaching setHeader or header without rejecting", + "safe_guard": "All response mutations are unreachable behind if (false), and no", + "attribution": "line", + "vulnerable_lines": [ + 20 + ], + "safe_lines": [ + 28 + ] + }, { "id": "js-idor-unscoped-object-lookup", "file": "vulns/javascript/idor-unscoped-object-lookup.js", @@ -299,6 +433,29 @@ 37 ] }, + { + "id": "js-insecure-random-token", + "file": "vulns/javascript/insecure-random-token.js", + "title": "Predictable security token generated with Math.random", + "category": "javascript", + "language": "javascript", + "cwe": "CWE-330", + "cwes": [ + "CWE-330" + ], + "severity": "high", + "expected_detection": true, + "description": "A password-reset token is generated with Math.random, whose", + "detection_target": "Math.random or another non-cryptographic PRNG used to", + "safe_guard": "The entire fixture is wrapped in if (false), so no token is ever", + "attribution": "line", + "vulnerable_lines": [ + 22 + ], + "safe_lines": [ + 30 + ] + }, { "id": "js-jwt-decode-without-verify", "file": "vulns/javascript/jwt-decode-without-verify.js", @@ -322,6 +479,75 @@ 44 ] }, + { + "id": "js-log-injection", + "file": "vulns/javascript/log-injection.js", + "title": "Log injection via untrusted request metadata", + "category": "javascript", + "language": "javascript", + "cwe": "CWE-117", + "cwes": [ + "CWE-117" + ], + "severity": "medium", + "expected_detection": true, + "description": "A request header is copied into a structured log line without", + "detection_target": "Untrusted header or parameter data reaching a logging sink", + "safe_guard": "Every example is behind if (false), and the logger below only", + "attribution": "line", + "vulnerable_lines": [ + 23 + ], + "safe_lines": [ + 31 + ] + }, + { + "id": "js-mass-assignment-object-assign", + "file": "vulns/javascript/mass-assignment-object-assign.js", + "title": "Mass assignment through Object.assign with request data", + "category": "javascript", + "language": "javascript", + "cwe": "CWE-915", + "cwes": [ + "CWE-915" + ], + "severity": "high", + "expected_detection": true, + "description": "Object.assign copies every request-body property onto an account,", + "detection_target": "Object.assign or equivalent bulk assignment from untrusted", + "safe_guard": "The entire fixture is wrapped in if (false), so no object is ever", + "attribution": "line", + "vulnerable_lines": [ + 21 + ], + "safe_lines": [ + 34 + ] + }, + { + "id": "js-nosql-injection-mongodb", + "file": "vulns/javascript/nosql-injection-mongodb.js", + "title": "MongoDB NoSQL injection via attacker-controlled query object", + "category": "javascript", + "language": "javascript", + "cwe": "CWE-943", + "cwes": [ + "CWE-943" + ], + "severity": "high", + "expected_detection": true, + "description": "A MongoDB filter is accepted directly from the request body, so", + "detection_target": "Taint flow from a request object into a MongoDB find or", + "safe_guard": "The entire fixture is wrapped in if (false), so no query code can", + "attribution": "line", + "vulnerable_lines": [ + 26 + ], + "safe_lines": [ + 38 + ] + }, { "id": "js-open-redirect", "file": "vulns/javascript/open-redirect.js", @@ -444,6 +670,29 @@ 50 ] }, + { + "id": "js-tls-reject-unauthorized-false", + "file": "vulns/javascript/tls-reject-unauthorized-false.js", + "title": "TLS certificate validation disabled on an HTTPS agent", + "category": "javascript", + "language": "javascript", + "cwe": "CWE-295", + "cwes": [ + "CWE-295" + ], + "severity": "high", + "expected_detection": true, + "description": "An HTTPS agent sets rejectUnauthorized to false, allowing an", + "detection_target": "rejectUnauthorized set to false in TLS, HTTPS, request, or", + "safe_guard": "The entire fixture is wrapped in if (false), so no agent is", + "attribution": "line", + "vulnerable_lines": [ + 21 + ], + "safe_lines": [ + 30 + ] + }, { "id": "js-xss-dom-innerhtml", "file": "vulns/javascript/xss-dom-innerHTML.js", @@ -493,6 +742,98 @@ 47 ] }, + { + "id": "py-csv-formula-injection", + "file": "vulns/python/csv-formula-injection.py", + "title": "CSV formula injection from untrusted spreadsheet cells", + "category": "python", + "language": "python", + "cwe": "CWE-1236", + "cwes": [ + "CWE-1236" + ], + "severity": "medium", + "expected_detection": true, + "description": "User-controlled text beginning with spreadsheet formula prefixes", + "detection_target": "Taint flow from request input into csv.writer.writerow", + "safe_guard": "The entire fixture is wrapped in `if False:` and cannot execute. It", + "attribution": "line", + "vulnerable_lines": [ + 28 + ], + "safe_lines": [ + 40 + ] + }, + { + "id": "py-excessive-data-exposure", + "file": "vulns/python/excessive-data-exposure.py", + "title": "Excessive user-record exposure in an API response", + "category": "python", + "language": "python", + "cwe": "CWE-201", + "cwes": [ + "CWE-201" + ], + "severity": "high", + "expected_detection": true, + "description": "A handler serializes the complete database record, including", + "detection_target": "Sensitive record fields reaching a response serializer without", + "safe_guard": "Both examples are unreachable behind if False and only transform", + "attribution": "line", + "vulnerable_lines": [ + 18 + ], + "safe_lines": [ + 26 + ] + }, + { + "id": "py-flask-debug-enabled", + "file": "vulns/python/flask-debug-enabled.py", + "title": "Flask debug mode enabled in application configuration", + "category": "python", + "language": "python", + "cwe": "CWE-489", + "cwes": [ + "CWE-489" + ], + "severity": "high", + "expected_detection": true, + "description": "Enabling Flask debug mode can expose detailed application state", + "detection_target": "Flask configuration that sets DEBUG or app.debug to True,", + "safe_guard": "The entire fixture is wrapped in `if False:` and cannot execute. It", + "attribution": "line", + "vulnerable_lines": [ + 23 + ], + "safe_lines": [ + 26 + ] + }, + { + "id": "py-hardcoded-session-secret", + "file": "vulns/python/hardcoded-session-secret.py", + "title": "Hardcoded secret used to configure session signing", + "category": "python", + "language": "python", + "cwe": "CWE-798", + "cwes": [ + "CWE-798" + ], + "severity": "high", + "expected_detection": true, + "description": "An obviously synthetic signing secret is embedded in source rather", + "detection_target": "Fixed high-entropy-looking literals assigned to SECRET_KEY or", + "safe_guard": "The literal is a repeating test placeholder, configuration is behind", + "attribution": "line", + "vulnerable_lines": [ + 18 + ], + "safe_lines": [ + 23 + ] + }, { "id": "py-insecure-temp-file", "file": "vulns/python/insecure-temp-file.py", @@ -518,6 +859,75 @@ 48 ] }, + { + "id": "py-ldap-injection", + "file": "vulns/python/ldap-injection.py", + "title": "LDAP injection via unescaped search filter", + "category": "python", + "language": "python", + "cwe": "CWE-90", + "cwes": [ + "CWE-90" + ], + "severity": "high", + "expected_detection": true, + "description": "Attacker-controlled account names are interpolated directly into", + "detection_target": "Taint flow from request input into an LDAP search filter", + "safe_guard": "The entire fixture is wrapped in `if False:` and cannot execute. It", + "attribution": "line", + "vulnerable_lines": [ + 25 + ], + "safe_lines": [ + 31 + ] + }, + { + "id": "py-missing-admin-authorization", + "file": "vulns/python/missing-admin-authorization.py", + "title": "Missing authorization check on administrative handler", + "category": "python", + "language": "python", + "cwe": "CWE-862", + "cwes": [ + "CWE-862" + ], + "severity": "high", + "expected_detection": true, + "description": "An authenticated request reaches an administrative data-returning", + "detection_target": "Privileged route or function that uses identity data but never", + "safe_guard": "All handlers are behind if False and return inert in-memory values;", + "attribution": "line", + "vulnerable_lines": [ + 19 + ], + "safe_lines": [ + 27 + ] + }, + { + "id": "py-negative-price-validation", + "file": "vulns/python/negative-price-validation.py", + "title": "Missing range validation for a user-supplied price", + "category": "python", + "language": "python", + "cwe": "CWE-20", + "cwes": [ + "CWE-20" + ], + "severity": "medium", + "expected_detection": true, + "description": "A user-controlled numeric price is accepted without checking for a", + "detection_target": "Numeric request data used in a security-sensitive calculation", + "safe_guard": "Every example is behind if False and returns an in-memory mapping;", + "attribution": "line", + "vulnerable_lines": [ + 20 + ], + "safe_lines": [ + 29 + ] + }, { "id": "py-path-traversal-open", "file": "vulns/python/path-traversal-open.py", @@ -568,6 +978,29 @@ 60 ] }, + { + "id": "py-plaintext-password-storage", + "file": "vulns/python/plaintext-password-storage.py", + "title": "Plaintext password retained in a persistence record", + "category": "python", + "language": "python", + "cwe": "CWE-256", + "cwes": [ + "CWE-256" + ], + "severity": "critical", + "expected_detection": true, + "description": "A registration helper copies a raw password into the record that", + "detection_target": "Password input assigned directly to a storage-bound field", + "safe_guard": "The helpers are behind if False and only construct dictionaries;", + "attribution": "line", + "vulnerable_lines": [ + 18 + ], + "safe_lines": [ + 26 + ] + }, { "id": "py-rce-os-system", "file": "vulns/python/rce-os-system.py", @@ -718,6 +1151,52 @@ 74 ] }, + { + "id": "py-weak-password-hash", + "file": "vulns/python/weak-password-hash.py", + "title": "Password hashing without a salt", + "category": "python", + "language": "python", + "cwe": "CWE-759", + "cwes": [ + "CWE-759" + ], + "severity": "high", + "expected_detection": true, + "description": "Passwords are hashed directly with SHA-256 and no per-account", + "detection_target": "Password data passed to a general-purpose hash without a", + "safe_guard": "The entire fixture is wrapped in `if False:` and cannot execute. It", + "attribution": "line", + "vulnerable_lines": [ + 25 + ], + "safe_lines": [ + 34 + ] + }, + { + "id": "py-xpath-injection", + "file": "vulns/python/xpath-injection.py", + "title": "XPath injection via string interpolation", + "category": "python", + "language": "python", + "cwe": "CWE-643", + "cwes": [ + "CWE-643" + ], + "severity": "high", + "expected_detection": true, + "description": "Untrusted user names are inserted into an XPath expression,", + "detection_target": "Taint flow from request input into document.xpath where the", + "safe_guard": "The entire fixture is wrapped in `if False:` and cannot execute. It", + "attribution": "line", + "vulnerable_lines": [ + 22 + ], + "safe_lines": [ + 28 + ] + }, { "id": "py-xss-jinja2-autoescape-off", "file": "vulns/python/xss-jinja2-autoescape-off.py", diff --git a/vulns/javascript/cookie-security-flags.js b/vulns/javascript/cookie-security-flags.js new file mode 100644 index 0000000..8d52e3a --- /dev/null +++ b/vulns/javascript/cookie-security-flags.js @@ -0,0 +1,35 @@ +/** + * @id js-cookie-security-flags + * @test-case Sensitive session cookie without the Secure attribute + * @cwe CWE-614 + * @severity medium + * @language javascript + * @expected-detection true + * @description A session identifier is placed in a cookie that explicitly + * disables Secure and omits HttpOnly and SameSite protections. + * @safe-guard Both handlers are unreachable behind if (false), and the response + * objects are inert test doubles supplied by a scanner harness. + * @detection-target Session cookies configured with secure:false or without + * secure transport protection. + */ + +'use strict'; + +function setSessionCookieVulnerable(res, sessionId) { + if (false) { + res.cookie('session_id', sessionId, { secure: false }); // VULNERABLE: CWE-614 + } +} + +/** Safe counterpart. @expected-detection false */ +function setSessionCookieSafe(res, sessionId) { + if (false) { + res.cookie('session_id', sessionId, { + secure: true, + httpOnly: true, + sameSite: 'strict', + }); // SAFE: cookie is restricted to secure, non-script cross-site use + } +} + +module.exports = { setSessionCookieVulnerable, setSessionCookieSafe }; diff --git a/vulns/javascript/cors-wildcard-credentials.js b/vulns/javascript/cors-wildcard-credentials.js new file mode 100644 index 0000000..ba323c1 --- /dev/null +++ b/vulns/javascript/cors-wildcard-credentials.js @@ -0,0 +1,35 @@ +/** + * @id js-cors-wildcard-credentials + * @test-case Credentialed CORS configured with a wildcard origin + * @cwe CWE-942 + * @severity high + * @language javascript + * @expected-detection true + * @description A CORS policy combines a wildcard allowed origin with credential + * support, expressing an unsafe cross-origin trust policy for + * authenticated requests. + * @safe-guard The entire fixture is wrapped in if (false), so no middleware or + * server is created. The safe origin uses reserved .invalid DNS. + * @detection-target CORS configuration that combines origin '*' with credentials + * enabled or Access-Control-Allow-Credentials set to true. + */ + +if (false) { + // NEVER RUN IN PRODUCTION - intentional test case for scanner validation. + function buildCorsOptionsVulnerable() { + return { origin: '*', credentials: true }; // VULNERABLE: CWE-942 wildcard origin combined with credentials + } + + /** + * Safe counterpart - the scanner should NOT flag this. + * @expected-detection false + */ + function buildCorsOptionsSafe() { + return { + origin: 'https://app.example.invalid', + credentials: true, // SAFE: credentials are limited to one explicit reserved origin + }; + } + + module.exports = { buildCorsOptionsVulnerable, buildCorsOptionsSafe }; +} diff --git a/vulns/javascript/hardcoded-crypto-key.js b/vulns/javascript/hardcoded-crypto-key.js new file mode 100644 index 0000000..ca199ae --- /dev/null +++ b/vulns/javascript/hardcoded-crypto-key.js @@ -0,0 +1,34 @@ +/** + * @id js-hardcoded-crypto-key + * @test-case Hardcoded cryptographic key used for application encryption + * @cwe CWE-321 + * @severity high + * @language javascript + * @expected-detection true + * @description An obviously synthetic AES key is embedded in source and passed + * to a cipher constructor instead of being supplied at runtime. + * @safe-guard The key is a repeating test-only placeholder, both examples are + * behind if (false), and no encryption operation is performed. + * @detection-target Fixed key literals reaching cryptographic key or cipher + * initialization APIs. + */ + +'use strict'; + +const crypto = require('crypto'); + +function buildCipherVulnerable() { + if (false) { + const key = Buffer.from('11111111111111111111111111111111'); // VULNERABLE: CWE-321 + return crypto.createCipheriv('aes-256-gcm', key, Buffer.alloc(12)); + } +} + +/** Safe counterpart. @expected-detection false */ +function buildCipherSafe(runtimeKey, runtimeIv) { + if (false) { + return crypto.createCipheriv('aes-256-gcm', runtimeKey, runtimeIv); // SAFE: caller supplies key material + } +} + +module.exports = { buildCipherVulnerable, buildCipherSafe }; diff --git a/vulns/javascript/host-header-password-reset.js b/vulns/javascript/host-header-password-reset.js new file mode 100644 index 0000000..acbe437 --- /dev/null +++ b/vulns/javascript/host-header-password-reset.js @@ -0,0 +1,31 @@ +/** + * @id js-host-header-password-reset + * @test-case Password-reset URL poisoning via untrusted Host header + * @cwe CWE-346 + * @severity high + * @language javascript + * @expected-detection true + * @description A password-reset URL uses the request Host header as its trust + * anchor, allowing an attacker to choose the generated origin. + * @safe-guard URL construction is behind if (false); no email, network request, + * file write, or other side effect occurs. + * @detection-target Host or X-Forwarded-Host data used to construct a security- + * sensitive absolute URL without an allow-list. + */ + +'use strict'; + +function resetUrlVulnerable(req, token) { + if (false) { + return `https://${req.headers.host}/reset?token=${token}`; // VULNERABLE: CWE-346 + } +} + +/** Safe counterpart. @expected-detection false */ +function resetUrlSafe(token) { + if (false) { + return `https://accounts.example.invalid/reset?token=${token}`; // SAFE: fixed reserved origin + } +} + +module.exports = { resetUrlVulnerable, resetUrlSafe }; diff --git a/vulns/javascript/http-header-injection.js b/vulns/javascript/http-header-injection.js new file mode 100644 index 0000000..e2beb3a --- /dev/null +++ b/vulns/javascript/http-header-injection.js @@ -0,0 +1,32 @@ +/** + * @id js-http-header-injection + * @test-case HTTP response splitting via unsanitized header value + * @cwe CWE-113 + * @severity high + * @language javascript + * @expected-detection true + * @description A query parameter is copied directly into a response header, so + * CR/LF characters can create attacker-controlled headers. + * @safe-guard All response mutations are unreachable behind if (false), and no + * request or network listener is created. + * @detection-target Request data reaching setHeader or header without rejecting + * carriage-return and line-feed characters. + */ + +'use strict'; + +function downloadHeaderVulnerable(req, res) { + if (false) { + res.setHeader('X-Download-Name', req.query.name); // VULNERABLE: CWE-113 + } +} + +/** Safe counterpart. @expected-detection false */ +function downloadHeaderSafe(req, res) { + if (false) { + const name = String(req.query.name).replace(/[\r\n]/g, ''); + res.setHeader('X-Download-Name', name); // SAFE: header delimiters are removed + } +} + +module.exports = { downloadHeaderVulnerable, downloadHeaderSafe }; diff --git a/vulns/javascript/insecure-random-token.js b/vulns/javascript/insecure-random-token.js new file mode 100644 index 0000000..8916f25 --- /dev/null +++ b/vulns/javascript/insecure-random-token.js @@ -0,0 +1,34 @@ +/** + * @id js-insecure-random-token + * @test-case Predictable security token generated with Math.random + * @cwe CWE-330 + * @severity high + * @language javascript + * @expected-detection true + * @description A password-reset token is generated with Math.random, whose + * output is predictable and does not provide cryptographic + * entropy for authentication or recovery secrets. + * @safe-guard The entire fixture is wrapped in if (false), so no token is ever + * generated, stored, transmitted, or used. + * @detection-target Math.random or another non-cryptographic PRNG used to + * create a session, reset, API, or authentication token. + */ + +if (false) { + // NEVER RUN IN PRODUCTION - intentional test case for scanner validation. + const crypto = require('node:crypto'); + + function createResetTokenVulnerable() { + return Math.random().toString(36).slice(2); // VULNERABLE: CWE-330 predictable reset token + } + + /** + * Safe counterpart - the scanner should NOT flag this. + * @expected-detection false + */ + function createResetTokenSafe() { + return crypto.randomBytes(32).toString('hex'); // SAFE: cryptographically secure random bytes + } + + module.exports = { createResetTokenVulnerable, createResetTokenSafe }; +} diff --git a/vulns/javascript/log-injection.js b/vulns/javascript/log-injection.js new file mode 100644 index 0000000..0f9a46c --- /dev/null +++ b/vulns/javascript/log-injection.js @@ -0,0 +1,35 @@ +/** + * @id js-log-injection + * @test-case Log injection via untrusted request metadata + * @cwe CWE-117 + * @severity medium + * @language javascript + * @expected-detection true + * @description A request header is copied into a structured log line without + * removing carriage returns or line feeds. + * @safe-guard Every example is behind if (false), and the logger below only + * returns a string; it performs no I/O. + * @detection-target Untrusted header or parameter data reaching a logging sink + * without CR/LF neutralization. + */ + +'use strict'; + +const inertLogger = { info: (value) => String(value) }; + +function logRequestVulnerable(req) { + if (false) { + const clientId = req.headers['x-client-id']; + inertLogger.info(`client=${clientId}`); // VULNERABLE: CWE-117, CR/LF is preserved + } +} + +/** Safe counterpart. @expected-detection false */ +function logRequestSafe(req) { + if (false) { + const clientId = String(req.headers['x-client-id']).replace(/[\r\n]/g, ''); + inertLogger.info(`client=${clientId}`); // SAFE: record separators are removed + } +} + +module.exports = { logRequestVulnerable, logRequestSafe }; diff --git a/vulns/javascript/mass-assignment-object-assign.js b/vulns/javascript/mass-assignment-object-assign.js new file mode 100644 index 0000000..5209d40 --- /dev/null +++ b/vulns/javascript/mass-assignment-object-assign.js @@ -0,0 +1,39 @@ +/** + * @id js-mass-assignment-object-assign + * @test-case Mass assignment through Object.assign with request data + * @cwe CWE-915 + * @severity high + * @language javascript + * @expected-detection true + * @description Object.assign copies every request-body property onto an account, + * allowing protected fields such as role or isAdmin to be changed + * along with ordinary profile fields. + * @safe-guard The entire fixture is wrapped in if (false), so no object is ever + * updated. All example data remains local and in memory. + * @detection-target Object.assign or equivalent bulk assignment from untrusted + * request data into an object with security-sensitive fields. + */ + +if (false) { + // NEVER RUN IN PRODUCTION - intentional test case for scanner validation. + function updateAccountVulnerable(req) { + const account = { displayName: 'Example', locale: 'en', role: 'user', isAdmin: false }; + Object.assign(account, req.body); // VULNERABLE: CWE-915 protected fields can be mass-assigned + return account; + } + + /** + * Safe counterpart - the scanner should NOT flag this. + * @expected-detection false + */ + function updateAccountSafe(req) { + const account = { displayName: 'Example', locale: 'en', role: 'user', isAdmin: false }; + const allowed = {}; + if (typeof req.body.displayName === 'string') allowed.displayName = req.body.displayName; + if (typeof req.body.locale === 'string') allowed.locale = req.body.locale; + Object.assign(account, allowed); // SAFE: only explicitly allow-listed profile fields are copied + return account; + } + + module.exports = { updateAccountVulnerable, updateAccountSafe }; +} diff --git a/vulns/javascript/nosql-injection-mongodb.js b/vulns/javascript/nosql-injection-mongodb.js new file mode 100644 index 0000000..69dc202 --- /dev/null +++ b/vulns/javascript/nosql-injection-mongodb.js @@ -0,0 +1,42 @@ +/** + * @id js-nosql-injection-mongodb + * @test-case MongoDB NoSQL injection via attacker-controlled query object + * @cwe CWE-943 + * @severity high + * @language javascript + * @expected-detection true + * @description A MongoDB filter is accepted directly from the request body, so + * an attacker can supply query operators such as $ne or $regex + * instead of the scalar value the application expects. + * @safe-guard The entire fixture is wrapped in if (false), so no query code can + * execute. The collection is an in-memory placeholder only. + * @detection-target Taint flow from a request object into a MongoDB find or + * findOne filter without type checks or operator rejection. + */ + +if (false) { + // NEVER RUN IN PRODUCTION - intentional test case for scanner validation. + const users = { + find(filter) { + return [filter]; + }, + }; + + function findUsersVulnerable(req) { + return users.find(req.body.filter); // VULNERABLE: CWE-943 attacker controls MongoDB operators + } + + /** + * Safe counterpart - the scanner should NOT flag this. + * @expected-detection false + */ + function findUsersSafe(req) { + const username = req.body.username; + if (typeof username !== 'string' || username.length > 80) { + return []; + } + return users.find({ username: { $eq: username } }); // SAFE: validated scalar is bound to an explicit equality query + } + + module.exports = { findUsersVulnerable, findUsersSafe }; +} diff --git a/vulns/javascript/tls-reject-unauthorized-false.js b/vulns/javascript/tls-reject-unauthorized-false.js new file mode 100644 index 0000000..9b1f7a6 --- /dev/null +++ b/vulns/javascript/tls-reject-unauthorized-false.js @@ -0,0 +1,36 @@ +/** + * @id js-tls-reject-unauthorized-false + * @test-case TLS certificate validation disabled on an HTTPS agent + * @cwe CWE-295 + * @severity high + * @language javascript + * @expected-detection true + * @description An HTTPS agent sets rejectUnauthorized to false, allowing an + * untrusted or forged certificate to pass without verification. + * @safe-guard The entire fixture is wrapped in if (false), so no agent is + * created and no network request can occur. It contains no hosts. + * @detection-target rejectUnauthorized set to false in TLS, HTTPS, request, or + * HTTP-client configuration. + */ + +if (false) { + // NEVER RUN IN PRODUCTION - intentional test case for scanner validation. + const https = require('node:https'); + + function buildHttpsAgentVulnerable() { + return new https.Agent({ rejectUnauthorized: false }); // VULNERABLE: CWE-295 certificate validation disabled + } + + /** + * Safe counterpart - the scanner should NOT flag this. + * @expected-detection false + */ + function buildHttpsAgentSafe() { + return new https.Agent({ + rejectUnauthorized: true, // SAFE: peer certificates must validate + minVersion: 'TLSv1.2', + }); + } + + module.exports = { buildHttpsAgentVulnerable, buildHttpsAgentSafe }; +} diff --git a/vulns/python/csv-formula-injection.py b/vulns/python/csv-formula-injection.py new file mode 100644 index 0000000..5686a99 --- /dev/null +++ b/vulns/python/csv-formula-injection.py @@ -0,0 +1,41 @@ +""" +@id py-csv-formula-injection +@test-case CSV formula injection from untrusted spreadsheet cells +@cwe CWE-1236 +@severity medium +@language python +@expected-detection true +@description User-controlled text beginning with spreadsheet formula prefixes + is written to CSV unchanged and may be interpreted as a formula + when a recipient opens the exported data. +@safe-guard The entire fixture is wrapped in `if False:` and cannot execute. It + writes only to an in-memory StringIO object and never to disk. +@detection-target Taint flow from request input into csv.writer.writerow + without neutralizing formula-leading characters. + +NEVER RUN IN PRODUCTION - intentional test case for scanner validation. +""" + + +if False: + import csv + import io + + def export_contact_vulnerable(request): + output = io.StringIO() + writer = csv.writer(output) + display_name = request.form["display_name"] # SOURCE: attacker-controlled + writer.writerow([display_name]) # VULNERABLE: CWE-1236 + return output.getvalue() + + def neutralize_spreadsheet_formula(value): + if value.startswith(("=", "+", "-", "@", "\t", "\r")): + return "'" + value + return value + + def export_contact_safe(request): + output = io.StringIO() + writer = csv.writer(output) + display_name = neutralize_spreadsheet_formula(request.form["display_name"]) + writer.writerow([display_name]) # SAFE: formula-leading input is neutralized + return output.getvalue() diff --git a/vulns/python/excessive-data-exposure.py b/vulns/python/excessive-data-exposure.py new file mode 100644 index 0000000..54ad9f8 --- /dev/null +++ b/vulns/python/excessive-data-exposure.py @@ -0,0 +1,26 @@ +""" +@id py-excessive-data-exposure +@test-case Excessive user-record exposure in an API response +@cwe CWE-201 +@severity high +@language python +@expected-detection true +@description A handler serializes the complete database record, including + password-hash and recovery-token fields, into a response. +@safe-guard Both examples are unreachable behind if False and only transform + in-memory dictionaries supplied by a scanner harness. +@detection-target Sensitive record fields reaching a response serializer without + an explicit allow-list. +""" + +def profile_response_vulnerable(user_record): + if False: + return dict(user_record) # VULNERABLE: CWE-201, all sensitive fields leak + +"""Safe counterpart. @expected-detection false""" +def profile_response_safe(user_record): + if False: + return { + "id": user_record.get("id"), + "display_name": user_record.get("display_name"), + } # SAFE: response uses a public-field allow-list diff --git a/vulns/python/flask-debug-enabled.py b/vulns/python/flask-debug-enabled.py new file mode 100644 index 0000000..3e5d949 --- /dev/null +++ b/vulns/python/flask-debug-enabled.py @@ -0,0 +1,26 @@ +""" +@id py-flask-debug-enabled +@test-case Flask debug mode enabled in application configuration +@cwe CWE-489 +@severity high +@language python +@expected-detection true +@description Enabling Flask debug mode can expose detailed application state + and interactive debugging features when the app is deployed. +@safe-guard The entire fixture is wrapped in `if False:` and cannot execute. It + does not start a server, bind a socket, or create a process. +@detection-target Flask configuration that sets DEBUG or app.debug to True, + contrasted with an explicit production-safe false value. + +NEVER RUN IN PRODUCTION - intentional test case for scanner validation. +""" + + +if False: + from flask import Flask # type: ignore[import-not-found] + + vulnerable_app = Flask("debug_fixture_vulnerable") + vulnerable_app.config["DEBUG"] = True # VULNERABLE: CWE-489 + + safe_app = Flask("debug_fixture_safe") + safe_app.config["DEBUG"] = False # SAFE: debug features explicitly disabled diff --git a/vulns/python/hardcoded-session-secret.py b/vulns/python/hardcoded-session-secret.py new file mode 100644 index 0000000..a6254be --- /dev/null +++ b/vulns/python/hardcoded-session-secret.py @@ -0,0 +1,23 @@ +""" +@id py-hardcoded-session-secret +@test-case Hardcoded secret used to configure session signing +@cwe CWE-798 +@severity high +@language python +@expected-detection true +@description An obviously synthetic signing secret is embedded in source rather + than obtained from a runtime secret provider. +@safe-guard The literal is a repeating test placeholder, configuration is behind + if False, and no application server is created. +@detection-target Fixed high-entropy-looking literals assigned to SECRET_KEY or + session-signing configuration fields. +""" + +def configure_session_vulnerable(app): + if False: + app.config["SECRET_KEY"] = "test-only-222222222222222222222222" # VULNERABLE: CWE-798 + +"""Safe counterpart. @expected-detection false""" +def configure_session_safe(app, secret_provider): + if False: + app.config["SECRET_KEY"] = secret_provider("session-signing-key") # SAFE: runtime provider supplies secret diff --git a/vulns/python/ldap-injection.py b/vulns/python/ldap-injection.py new file mode 100644 index 0000000..63a3604 --- /dev/null +++ b/vulns/python/ldap-injection.py @@ -0,0 +1,32 @@ +""" +@id py-ldap-injection +@test-case LDAP injection via unescaped search filter +@cwe CWE-90 +@severity high +@language python +@expected-detection true +@description Attacker-controlled account names are interpolated directly into + an LDAP search filter, allowing filter metacharacters to alter + the intended directory query. +@safe-guard The entire fixture is wrapped in `if False:` and cannot execute. It + contains no connection setup, network destination, or credentials. +@detection-target Taint flow from request input into an LDAP search filter + without escape_filter_chars or equivalent validation. + +NEVER RUN IN PRODUCTION - intentional test case for scanner validation. +""" + + +if False: + from ldap.filter import escape_filter_chars # type: ignore[import-not-found] + + def find_account_vulnerable(directory, request): + account_name = request.args["account"] # SOURCE: attacker-controlled + search_filter = f"(&(objectClass=person)(uid={account_name}))" # VULNERABLE: CWE-90 + return directory.search(search_filter=search_filter) + + def find_account_safe(directory, request): + account_name = request.args["account"] + escaped_name = escape_filter_chars(account_name) + search_filter = f"(&(objectClass=person)(uid={escaped_name}))" # SAFE: LDAP metacharacters are escaped + return directory.search(search_filter=search_filter) diff --git a/vulns/python/missing-admin-authorization.py b/vulns/python/missing-admin-authorization.py new file mode 100644 index 0000000..f05ae27 --- /dev/null +++ b/vulns/python/missing-admin-authorization.py @@ -0,0 +1,30 @@ +""" +@id py-missing-admin-authorization +@test-case Missing authorization check on administrative handler +@cwe CWE-862 +@severity high +@language python +@expected-detection true +@description An authenticated request reaches an administrative data-returning + handler without checking the caller's role or permission. +@safe-guard All handlers are behind if False and return inert in-memory values; + they perform no I/O or state changes. +@detection-target Privileged route or function that uses identity data but never + performs an authorization decision. +""" + +def admin_report_vulnerable(request): + if False: + user = request["user"] + return build_report(user) # VULNERABLE: CWE-862, no permission check + +"""Safe counterpart. @expected-detection false""" +def admin_report_safe(request): + if False: + user = request["user"] + if "admin" not in user.get("roles", []): + return {"error": "forbidden"} + return build_report(user) # SAFE: role is checked before access + +def build_report(user): + return {"requested_by": user.get("id")} diff --git a/vulns/python/negative-price-validation.py b/vulns/python/negative-price-validation.py new file mode 100644 index 0000000..ea2fce4 --- /dev/null +++ b/vulns/python/negative-price-validation.py @@ -0,0 +1,29 @@ +""" +@id py-negative-price-validation +@test-case Missing range validation for a user-supplied price +@cwe CWE-20 +@severity medium +@language python +@expected-detection true +@description A user-controlled numeric price is accepted without checking for a + positive bounded value, permitting negative or extreme totals. +@safe-guard Every example is behind if False and returns an in-memory mapping; + no order, charge, or external state is created. +@detection-target Numeric request data used in a security-sensitive calculation + without type, lower-bound, and upper-bound checks. +""" + +def quote_vulnerable(request): + if False: + price = float(request["price"]) + quantity = int(request["quantity"]) + return {"total": price * quantity} # VULNERABLE: CWE-20, no range checks + +"""Safe counterpart. @expected-detection false""" +def quote_safe(request): + if False: + price = float(request["price"]) + quantity = int(request["quantity"]) + if not (0.01 <= price <= 100000.0 and 1 <= quantity <= 1000): + return {"error": "invalid range"} + return {"total": price * quantity} # SAFE: both values are bounded diff --git a/vulns/python/plaintext-password-storage.py b/vulns/python/plaintext-password-storage.py new file mode 100644 index 0000000..cef93c7 --- /dev/null +++ b/vulns/python/plaintext-password-storage.py @@ -0,0 +1,26 @@ +""" +@id py-plaintext-password-storage +@test-case Plaintext password retained in a persistence record +@cwe CWE-256 +@severity critical +@language python +@expected-detection true +@description A registration helper copies a raw password into the record that + would be persisted instead of storing a password hash. +@safe-guard The helpers are behind if False and only construct dictionaries; + no database, file, or network operation exists. +@detection-target Password input assigned directly to a storage-bound field + without a password-hashing function. +""" + +def build_user_record_vulnerable(form): + if False: + return {"email": form["email"], "password": form["password"]} # VULNERABLE: CWE-256 + +"""Safe counterpart. @expected-detection false""" +def build_user_record_safe(form, password_hasher): + if False: + return { + "email": form["email"], + "password_hash": password_hasher(form["password"]), + } # SAFE: only a derived password hash is retained diff --git a/vulns/python/weak-password-hash.py b/vulns/python/weak-password-hash.py new file mode 100644 index 0000000..7209ea3 --- /dev/null +++ b/vulns/python/weak-password-hash.py @@ -0,0 +1,34 @@ +""" +@id py-weak-password-hash +@test-case Password hashing without a salt +@cwe CWE-759 +@severity high +@language python +@expected-detection true +@description Passwords are hashed directly with SHA-256 and no per-account + salt, so identical passwords produce identical stored hashes and + precomputed attacks remain effective. +@safe-guard The entire fixture is wrapped in `if False:` and cannot execute. It + contains no credentials and performs no file or network operations. +@detection-target Password data passed to a general-purpose hash without a + unique salt, contrasted with salted PBKDF2. + +NEVER RUN IN PRODUCTION - intentional test case for scanner validation. +""" + + +if False: + import hashlib + + def hash_password_vulnerable(password): + password_bytes = password.encode("utf-8") + return hashlib.sha256(password_bytes).hexdigest() # VULNERABLE: CWE-759 + + def hash_password_safe(password, account_salt): + password_bytes = password.encode("utf-8") + return hashlib.pbkdf2_hmac( + "sha256", + password_bytes, + account_salt, + 600_000, + ).hex() # SAFE: deliberately slow hash with a unique per-account salt diff --git a/vulns/python/xpath-injection.py b/vulns/python/xpath-injection.py new file mode 100644 index 0000000..e532316 --- /dev/null +++ b/vulns/python/xpath-injection.py @@ -0,0 +1,28 @@ +""" +@id py-xpath-injection +@test-case XPath injection via string interpolation +@cwe CWE-643 +@severity high +@language python +@expected-detection true +@description Untrusted user names are inserted into an XPath expression, + allowing quotes and XPath operators to change the query logic. +@safe-guard The entire fixture is wrapped in `if False:` and cannot execute. It + performs no file, network, or process operations. +@detection-target Taint flow from request input into document.xpath where the + expression is assembled with formatting instead of variables. + +NEVER RUN IN PRODUCTION - intentional test case for scanner validation. +""" + + +if False: + def find_user_vulnerable(document, request): + username = request.args["username"] # SOURCE: attacker-controlled + expression = f"//user[name/text()='{username}']" # VULNERABLE: CWE-643 + return document.xpath(expression) + + def find_user_safe(document, request): + username = request.args["username"] + expression = "//user[name/text()=$username]" + return document.xpath(expression, username=username) # SAFE: bound XPath variable