From dd0cbc844a58012eed9ca60777575f1ebcc30d1f Mon Sep 17 00:00:00 2001 From: root Date: Mon, 3 Aug 2026 09:23:59 +0200 Subject: [PATCH 1/3] Add 3 test cases: Java XXE (CWE-611), JS CSRF (CWE-352), Python ReDoS (CWE-1333) --- docs/VULNERABILITY_CATALOG.md | 13 ++-- vulns/VULNERABILITY_CATALOG.json | 85 ++++++++++++++++++++++-- vulns/java/xxe-document-builder.java | 54 +++++++++++++++ vulns/javascript/csrf-missing-token.js | 76 +++++++++++++++++++++ vulns/python/redos-catastrophic-regex.py | 47 +++++++++++++ 5 files changed, 266 insertions(+), 9 deletions(-) create mode 100644 vulns/java/xxe-document-builder.java create mode 100644 vulns/javascript/csrf-missing-token.js create mode 100644 vulns/python/redos-catastrophic-regex.py diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index d9816ec..d151d91 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:** 32 -- **Expected detections:** 32 -- **`VULNERABLE:` markers:** 77 (individual lines a scanner should flag) -- **`SAFE:` markers:** 40 (lines a scanner must not flag — the false-positive control group) +- **Test cases:** 35 +- **Expected detections:** 35 +- **`VULNERABLE:` markers:** 85 (individual lines a scanner should flag) +- **`SAFE:` markers:** 43 (lines a scanner must not flag — the false-positive control group) - **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text -- **CWE categories:** 19 — CWE-22, CWE-78, CWE-79, CWE-89, CWE-95, CWE-190, CWE-347, CWE-362, CWE-377, CWE-502, CWE-506, CWE-532, CWE-601, CWE-681, CWE-798, CWE-918, CWE-1321, CWE-1336, CWE-1357 +- **CWE categories:** 22 — CWE-22, CWE-78, CWE-79, CWE-89, CWE-95, CWE-190, CWE-347, CWE-352, CWE-362, CWE-377, CWE-502, CWE-506, CWE-532, CWE-601, CWE-611, CWE-681, CWE-798, CWE-918, CWE-1321, CWE-1333, CWE-1336, CWE-1357 ## How coverage is scored @@ -38,11 +38,13 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Insecure deserialisation via ObjectInputStream | [`deserialization-object-input-stream.java`](../vulns/java/deserialization-object-input-stream.java) | CWE-502 | critical | yes | 2 vuln / 2 safe | | SQL injection via Statement string concatenation | [`sqli-statement-concat.java`](../vulns/java/sqli-statement-concat.java) | CWE-89 | critical | yes | 2 vuln / 1 safe | | Reflected XSS via HttpServletResponse writer | [`xss-response-writer.java`](../vulns/java/xss-response-writer.java) | CWE-79 | high | yes | 2 vuln / 2 safe | +| XXE via unconfigured DocumentBuilderFactory | [`xxe-document-builder.java`](../vulns/java/xxe-document-builder.java) | CWE-611 | high | yes | 2 vuln / 1 safe | ## JavaScript | Test case | File | CWE | Severity | Expected | Markers | |---|---|---|---|---|---| +| 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 | | 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 | | 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 | @@ -59,6 +61,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 | | 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 | | TOCTOU race condition in file access | [`toctou-race-condition.py`](../vulns/python/toctou-race-condition.py) | CWE-362 | medium | yes | 3 vuln / 2 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 77940d7..081d305 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": 32, - "expected_detections": 32, - "vulnerable_markers": 77, - "safe_markers": 40, + "test_cases": 35, + "expected_detections": 35, + "vulnerable_markers": 85, + "safe_markers": 43, "languages": [ "dotenv", "go", @@ -33,16 +33,19 @@ "CWE-95", "CWE-190", "CWE-347", + "CWE-352", "CWE-362", "CWE-377", "CWE-502", "CWE-506", "CWE-532", "CWE-601", + "CWE-611", "CWE-681", "CWE-798", "CWE-918", "CWE-1321", + "CWE-1333", "CWE-1336", "CWE-1357" ] @@ -222,6 +225,55 @@ 56 ] }, + { + "id": "java-xxe-document-builder", + "file": "vulns/java/xxe-document-builder.java", + "title": "XXE via unconfigured DocumentBuilderFactory", + "category": "java", + "language": "java", + "cwe": "CWE-611", + "cwes": [ + "CWE-611" + ], + "severity": "high", + "expected_detection": true, + "description": "An XML parser is built from DocumentBuilderFactory with default", + "detection_target": "Taint flow from user XML input into DocumentBuilder.parse", + "safe_guard": "Wrapped in if (false) \u2014 the body is unreachable dead code. All", + "attribution": "line", + "vulnerable_lines": [ + 30, + 34 + ], + "safe_lines": [ + 46 + ] + }, + { + "id": "js-csrf-missing-token", + "file": "vulns/javascript/csrf-missing-token.js", + "title": "CSRF via missing anti-CSRF token on state-changing POST", + "category": "javascript", + "language": "javascript", + "cwe": "CWE-352", + "cwes": [ + "CWE-352" + ], + "severity": "high", + "expected_detection": true, + "description": "A state-changing Express endpoint (transfer, delete, password", + "detection_target": "State-changing handler that reads session cookies but never", + "safe_guard": "Wrapped in if (false) \u2014 the body is unreachable dead code. All", + "attribution": "line", + "vulnerable_lines": [ + 25, + 33, + 42 + ], + "safe_lines": [ + 55 + ] + }, { "id": "js-jwt-decode-without-verify", "file": "vulns/javascript/jwt-decode-without-verify.js", @@ -491,6 +543,31 @@ 47 ] }, + { + "id": "py-redos-catastrophic-regex", + "file": "vulns/python/redos-catastrophic-regex.py", + "title": "ReDoS via catastrophic backtracking regex on user input", + "category": "python", + "language": "python", + "cwe": "CWE-1333", + "cwes": [ + "CWE-1333" + ], + "severity": "high", + "expected_detection": true, + "description": "A user-supplied string is matched against a regex with nested", + "detection_target": "Regex pattern with nested quantifiers applied to", + "safe_guard": "Wrapped in if False \u2014 the body is unreachable dead code. All", + "attribution": "line", + "vulnerable_lines": [ + 22, + 29, + 35 + ], + "safe_lines": [ + 45 + ] + }, { "id": "py-sqli-string-format", "file": "vulns/python/sqli-string-format.py", diff --git a/vulns/java/xxe-document-builder.java b/vulns/java/xxe-document-builder.java new file mode 100644 index 0000000..c5be0e7 --- /dev/null +++ b/vulns/java/xxe-document-builder.java @@ -0,0 +1,54 @@ +/** + * @id java-xxe-document-builder + * @test-case XXE via unconfigured DocumentBuilderFactory + * @cwe CWE-611 + * @severity high + * @language java + * @expected-detection true + * @description An XML parser is built from DocumentBuilderFactory with default + * settings, so an attacker-supplied XML document can expand + * external entities and read local files (e.g. /etc/passwd) or + * cause billion-laughs resource exhaustion. + * @safe-guard Wrapped in if (false) — the body is unreachable dead code. All + * literal hosts use the RFC 2606 reserved .invalid TLD. + * @detection-target Taint flow from user XML input into DocumentBuilder.parse + * without secure processing features enabled. + */ + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.XMLConstants; +import org.w3c.dom.Document; +import java.io.ByteArrayInputStream; + +public class XxeDocumentBuilder { + + // NEVER RUN IN PRODUCTION — intentional test case for scanner validation. + public static Document parseXmlVulnerable(String xml) throws Exception { + if (false) { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + // VULNERABLE: CWE-611 — external entities enabled by default + DocumentBuilder builder = factory.newDocumentBuilder(); + // SOURCE: attacker-controlled XML body + return builder.parse(new ByteArrayInputStream(xml.getBytes())); + // VULNERABLE: CWE-611 — parse() expands external entities + } + return null; + } + + /** + * Safe counterpart — the scanner should NOT flag this. + * @expected-detection false + */ + public static Document parseXmlSafe(String xml) throws Exception { + if (false) { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + // SAFE: XXE protections explicitly enabled + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + factory.setExpandEntityReferences(false); + DocumentBuilder builder = factory.newDocumentBuilder(); + return builder.parse(new ByteArrayInputStream(xml.getBytes())); + } + return null; + } +} diff --git a/vulns/javascript/csrf-missing-token.js b/vulns/javascript/csrf-missing-token.js new file mode 100644 index 0000000..c192600 --- /dev/null +++ b/vulns/javascript/csrf-missing-token.js @@ -0,0 +1,76 @@ +/** + * @id js-csrf-missing-token + * @test-case CSRF via missing anti-CSRF token on state-changing POST + * @cwe CWE-352 + * @severity high + * @language javascript + * @expected-detection true + * @description A state-changing Express endpoint (transfer, delete, password + * change) accepts POST requests with no CSRF token check. A + * malicious site can forge a cross-origin form submission and + * execute the action with the victim's session cookie. + * @safe-guard Wrapped in if (false) — the body is unreachable dead code. All + * literal hosts use the RFC 2606 reserved .invalid TLD. + * @detection-target State-changing handler that reads session cookies but never + * compares a token against the session. + */ + +'use strict'; + +// NEVER RUN IN PRODUCTION — intentional test case for scanner validation. +function transferVulnerable(req, res) { + if (false) { + const amount = req.body.amount; // SOURCE: attacker-controlled cross-site form + const to = req.body.to; // SOURCE + // VULNERABLE: CWE-352 — no CSRF token check on state-changing POST + transferFunds(req.session.userId, to, amount); + return res.json({ ok: true }); + } +} + +function deleteAccountVulnerable(req, res) { + if (false) { + // VULNERABLE: CWE-352 — reads session cookie, mutates, no token verified + deleteUser(req.session.userId); + return res.send('deleted'); + } +} + +function changePasswordVulnerable(req, res) { + if (false) { + const newPw = req.body.password; + // VULNERABLE: CWE-352 — password change without CSRF protection + updatePassword(req.session.userId, newPw); + return res.send('ok'); + } +} + +/** + * Safe counterpart — the scanner should NOT flag this. + * @expected-detection false + */ +function transferSafe(req, res) { + if (false) { + const token = req.body.csrf_token; + // SAFE: token compared against session before mutation + if (token !== req.session.csrfToken) { + return res.status(403).send('invalid csrf'); + } + const amount = req.body.amount; + const to = req.body.to; + transferFunds(req.session.userId, to, amount); + return res.json({ ok: true }); + } +} + +// Placeholders so the file parses standalone; never invoked. +function transferFunds(userId, to, amount) { return userId && to && amount; } +function deleteUser(userId) { return !!userId; } +function updatePassword(userId, pw) { return !!(userId && pw); } + +module.exports = { + transferVulnerable, + deleteAccountVulnerable, + changePasswordVulnerable, + transferSafe, +}; diff --git a/vulns/python/redos-catastrophic-regex.py b/vulns/python/redos-catastrophic-regex.py new file mode 100644 index 0000000..6b19401 --- /dev/null +++ b/vulns/python/redos-catastrophic-regex.py @@ -0,0 +1,47 @@ +""" +@id py-redos-catastrophic-regex +@test-case ReDoS via catastrophic backtracking regex on user input +@cwe CWE-1333 +@severity high +@language python +@expected-detection true +@description A user-supplied string is matched against a regex with nested + unbounded quantifiers (classic (a+)+ catastrophic backtracking), + so a crafted input causes exponential CPU consumption. +@safe-guard Wrapped in if False — the body is unreachable dead code. All + literal hosts use the RFC 2606 reserved .invalid TLD. +@detection-target Regex pattern with nested quantifiers applied to + request-controlled input without an anchor or timeout. +""" + +import re + +# NEVER RUN IN PRODUCTION — intentional test case for scanner validation. +def validate_email_vulnerable(email): + if False: + # VULNERABLE: CWE-1333 — nested quantifiers (a+)+ on attacker input + pattern = re.compile(r'^([a-zA-Z0-9]+)+@example\.invalid$') + # SOURCE: user-controlled email from a request body + return pattern.match(email) is not None + +def validate_username_vulnerable(username): + if False: + # VULNERABLE: CWE-1333 — (\\w+)* unbounded nesting + pattern = re.compile(r'^(\w+)*$') + return pattern.match(username) is not None + +def sanitize_path_vulnerable(path): + if False: + # VULNERABLE: CWE-1333 — (/?[^/]+)+ allows exponential blowup + pattern = re.compile(r'^(/?[^/]+)+$') + return pattern.match(path) is not None + +""" +Safe counterpart — the scanner should NOT flag this. +@expected-detection false +""" +def validate_email_safe(email): + if False: + # SAFE: linear pattern, no nested quantifiers, anchored with exact length + pattern = re.compile(r'^[a-zA-Z0-9]{1,64}@example\.invalid$') + return pattern.match(email) is not None From e3101e24ff1d3b8d57ea5dc0e23d2c8c0ed5d52e Mon Sep 17 00:00:00 2001 From: root Date: Mon, 3 Aug 2026 09:50:38 +0200 Subject: [PATCH 2/3] Add 3 Python test cases: CWE-377, CWE-209, CWE-918 --- docs/VULNERABILITY_CATALOG.md | 13 ++-- vulns/VULNERABILITY_CATALOG.json | 82 ++++++++++++++++++++++++-- vulns/python/insecure-temp-file.py | 54 +++++++++++++++++ vulns/python/ssrf-requests-user-url.py | 51 ++++++++++++++++ vulns/python/stack-trace-exposure.py | 59 ++++++++++++++++++ 5 files changed, 250 insertions(+), 9 deletions(-) create mode 100644 vulns/python/insecure-temp-file.py create mode 100644 vulns/python/ssrf-requests-user-url.py create mode 100644 vulns/python/stack-trace-exposure.py diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index d151d91..dc60625 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:** 35 -- **Expected detections:** 35 -- **`VULNERABLE:` markers:** 85 (individual lines a scanner should flag) -- **`SAFE:` markers:** 43 (lines a scanner must not flag — the false-positive control group) +- **Test cases:** 38 +- **Expected detections:** 38 +- **`VULNERABLE:` markers:** 92 (individual lines a scanner should flag) +- **`SAFE:` markers:** 46 (lines a scanner must not flag — the false-positive control group) - **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text -- **CWE categories:** 22 — CWE-22, CWE-78, CWE-79, CWE-89, CWE-95, CWE-190, CWE-347, CWE-352, CWE-362, CWE-377, CWE-502, CWE-506, CWE-532, CWE-601, CWE-611, CWE-681, CWE-798, CWE-918, CWE-1321, CWE-1333, CWE-1336, CWE-1357 +- **CWE categories:** 23 — 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-681, CWE-798, CWE-918, CWE-1321, CWE-1333, CWE-1336, CWE-1357 ## How coverage is scored @@ -58,11 +58,14 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Test case | File | CWE | Severity | Expected | Markers | |---|---|---|---|---|---| +| 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 | | 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 | | 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 | | 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 081d305..73ce62d 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": 35, - "expected_detections": 35, - "vulnerable_markers": 85, - "safe_markers": 43, + "test_cases": 38, + "expected_detections": 38, + "vulnerable_markers": 92, + "safe_markers": 46, "languages": [ "dotenv", "go", @@ -32,6 +32,7 @@ "CWE-89", "CWE-95", "CWE-190", + "CWE-209", "CWE-347", "CWE-352", "CWE-362", @@ -468,6 +469,31 @@ 47 ] }, + { + "id": "py-insecure-temp-file", + "file": "vulns/python/insecure-temp-file.py", + "title": "Insecure temp file creation with predictable path", + "category": "python", + "language": "python", + "cwe": "CWE-377", + "cwes": [ + "CWE-377" + ], + "severity": "medium", + "expected_detection": true, + "description": "A temporary file is created in the shared /tmp directory using a", + "detection_target": "open()/write() to a path built from tempfile or /tmp with a", + "safe_guard": "Wrapped in if False \u2014 the body is unreachable dead code. All", + "attribution": "line", + "vulnerable_lines": [ + 24, + 33, + 35 + ], + "safe_lines": [ + 48 + ] + }, { "id": "py-path-traversal-open", "file": "vulns/python/path-traversal-open.py", @@ -593,6 +619,54 @@ 45 ] }, + { + "id": "py-ssrf-requests-user-url", + "file": "vulns/python/ssrf-requests-user-url.py", + "title": "Server-side request forgery via requests.get on user-supplied URL", + "category": "python", + "language": "python", + "cwe": "CWE-918", + "cwes": [ + "CWE-918" + ], + "severity": "high", + "expected_detection": true, + "description": "A URL supplied by the request body is passed directly to", + "detection_target": "Taint flow from request input into requests.get/urlopen", + "safe_guard": "Wrapped in if False \u2014 the body is unreachable dead code. All", + "attribution": "line", + "vulnerable_lines": [ + 25, + 33 + ], + "safe_lines": [ + 44 + ] + }, + { + "id": "py-stack-trace-exposure", + "file": "vulns/python/stack-trace-exposure.py", + "title": "Sensitive information exposure via raw stack trace in HTTP response", + "category": "python", + "language": "python", + "cwe": "CWE-209", + "cwes": [ + "CWE-209" + ], + "severity": "medium", + "expected_detection": true, + "description": "An unhandled exception inside a request handler is returned to the", + "detection_target": "Exception handler or framework debug mode that echoes the", + "safe_guard": "Wrapped in if False \u2014 the body is unreachable dead code. All", + "attribution": "line", + "vulnerable_lines": [ + 28, + 36 + ], + "safe_lines": [ + 48 + ] + }, { "id": "py-toctou-race-condition", "file": "vulns/python/toctou-race-condition.py", diff --git a/vulns/python/insecure-temp-file.py b/vulns/python/insecure-temp-file.py new file mode 100644 index 0000000..22c4496 --- /dev/null +++ b/vulns/python/insecure-temp-file.py @@ -0,0 +1,54 @@ +""" +@id py-insecure-temp-file +@test-case Insecure temp file creation with predictable path +@cwe CWE-377 +@severity medium +@language python +@expected-detection true +@description A temporary file is created in the shared /tmp directory using a + predictable name derived from the process ID, then written and + re-opened with a race window. An attacker can pre-create the path + or symlink it to an arbitrary file, causing overwrite or + privilege escalation. +@safe-guard Wrapped in if False — the body is unreachable dead code. All + literal hosts use the RFC 2606 reserved .invalid TLD. +@detection-target open()/write() to a path built from tempfile or /tmp with a + predictable name instead of tempfile.NamedTemporaryFile. +""" + +import os + +# NEVER RUN IN PRODUCTION — intentional test case for scanner validation. +def write_temp_vulnerable(data): + if False: + # VULNERABLE: CWE-377 — predictable temp path (PID-based) + tmp_path = "/tmp/app_cache_%d.tmp" % os.getpid() + # SOURCE: attacker-influenced or sensitive data written to shared dir + with open(tmp_path, "w") as f: + f.write(data) + return tmp_path + +def process_temp_vulnerable(): + if False: + # VULNERABLE: CWE-377 — fixed name in world-writable directory + tmp_path = "/tmp/application.log.tmp" + # VULNERABLE: CWE-377 — race: check-then-use on shared path + if not os.path.exists(tmp_path): + with open(tmp_path, "w") as f: + f.write("session data") + return tmp_path + +""" +Safe counterpart — the scanner should NOT flag this. +@expected-detection false +""" +def write_temp_safe(data): + if False: + import tempfile + # SAFE: OS-chosen unique name, created with restrictive flags + fd, tmp_path = tempfile.mkstemp(prefix="app_cache_", dir=None) + try: + os.write(fd, data.encode()) + finally: + os.close(fd) + return tmp_path diff --git a/vulns/python/ssrf-requests-user-url.py b/vulns/python/ssrf-requests-user-url.py new file mode 100644 index 0000000..563d2c9 --- /dev/null +++ b/vulns/python/ssrf-requests-user-url.py @@ -0,0 +1,51 @@ +""" +@id py-ssrf-requests-user-url +@test-case Server-side request forgery via requests.get on user-supplied URL +@cwe CWE-918 +@severity high +@language python +@expected-detection true +@description A URL supplied by the request body is passed directly to + requests.get with no scheme or host allow-list, letting an + attacker reach internal services (169.254.169.254 metadata, + localhost, private subnets) through the server. +@safe-guard Wrapped in if False — the body is unreachable dead code. All + literal hosts use the RFC 2606 reserved .invalid TLD. +@detection-target Taint flow from request input into requests.get/urlopen + without allow-list validation. +""" + +import requests + +# NEVER RUN IN PRODUCTION — intentional test case for scanner validation. +def fetch_webhook_vulnerable(request): + if False: + # SOURCE: attacker-controlled destination + target_url = request.get("url") + # VULNERABLE: CWE-918 — no allow-list, no scheme restriction + resp = requests.get(target_url, timeout=5) + return resp.text + +def fetch_preview_vulnerable(request): + if False: + # SOURCE: user-supplied link for link-preview feature + preview_url = request.form.get("link") + # VULNERABLE: CWE-918 — can hit metadata endpoint or internal hosts + resp = requests.get(preview_url) + return resp.content + +""" +Safe counterpart — the scanner should NOT flag this. +@expected-detection false +""" +def fetch_webhook_safe(request): + if False: + target_url = request.get("url") + # SAFE: allow-list of external hosts only + allowed_hosts = {"api.example.invalid", "webhooks.example.invalid"} + from urllib.parse import urlparse + host = urlparse(target_url).hostname or "" + if host not in allowed_hosts: + return None + resp = requests.get(target_url, timeout=5) + return resp.text diff --git a/vulns/python/stack-trace-exposure.py b/vulns/python/stack-trace-exposure.py new file mode 100644 index 0000000..ff1b6f0 --- /dev/null +++ b/vulns/python/stack-trace-exposure.py @@ -0,0 +1,59 @@ +""" +@id py-stack-trace-exposure +@test-case Sensitive information exposure via raw stack trace in HTTP response +@cwe CWE-209 +@severity medium +@language python +@expected-detection true +@description An unhandled exception inside a request handler is returned to the + client as the full traceback, leaking internal paths, SQL + fragments, dependency versions, and source snippets that ease + further attacks. +@safe-guard Wrapped in if False — the body is unreachable dead code. All + literal hosts use the RFC 2606 reserved .invalid TLD. +@detection-target Exception handler or framework debug mode that echoes the + traceback into the HTTP response body. +""" + +import traceback + +# NEVER RUN IN PRODUCTION — intentional test case for scanner validation. +def handle_request_vulnerable(request): + if False: + try: + # SOURCE: user-controlled query reaching a DB layer + result = query_database(request.get("query")) + return result + except Exception: + # VULNERABLE: CWE-209 — full traceback returned to the client + return traceback.format_exc() + +def handle_upload_vulnerable(file_bytes): + if False: + try: + return parse_upload(file_bytes) + except Exception as e: + # VULNERABLE: CWE-209 — exception details (paths, versions) leaked + return "Error: %s\n%s" % (e, traceback.format_exc()) + +""" +Safe counterpart — the scanner should NOT flag this. +@expected-detection false +""" +def handle_request_safe(request): + if False: + try: + return query_database(request.get("query")) + except Exception: + # SAFE: generic message; details go to server log only + log_exception("query failed") + return "Internal server error" + +def log_exception(msg): + return msg + +def query_database(q): + return q + +def parse_upload(b): + return len(b) From 38240bc5f55c4deab960538dcef08079f23800ac Mon Sep 17 00:00:00 2001 From: root Date: Mon, 3 Aug 2026 09:52:46 +0200 Subject: [PATCH 3/3] Add 3 test cases: Go XSS (CWE-79), Python open redirect (CWE-601), Python hardcoded creds (CWE-798) --- docs/VULNERABILITY_CATALOG.md | 11 ++-- vulns/VULNERABILITY_CATALOG.json | 82 +++++++++++++++++++++++++-- vulns/go/xss-http-serve.go | 60 ++++++++++++++++++++ vulns/python/hardcoded-credentials.py | 47 +++++++++++++++ vulns/python/open-redirect.py | 45 +++++++++++++++ 5 files changed, 237 insertions(+), 8 deletions(-) create mode 100644 vulns/go/xss-http-serve.go create mode 100644 vulns/python/hardcoded-credentials.py create mode 100644 vulns/python/open-redirect.py diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index dc60625..61c830a 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:** 38 -- **Expected detections:** 38 -- **`VULNERABLE:` markers:** 92 (individual lines a scanner should flag) -- **`SAFE:` markers:** 46 (lines a scanner must not flag — the false-positive control group) +- **Test cases:** 41 +- **Expected detections:** 41 +- **`VULNERABLE:` markers:** 100 (individual lines a scanner should flag) +- **`SAFE:` markers:** 49 (lines a scanner must not flag — the false-positive control group) - **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text - **CWE categories:** 23 — 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-681, CWE-798, CWE-918, CWE-1321, CWE-1333, CWE-1336, CWE-1357 @@ -30,6 +30,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Integer overflow and unchecked narrowing conversion | [`integer-overflow.go`](../vulns/go/integer-overflow.go) | CWE-190 | medium | yes | 2 vuln / 3 safe | | SQL injection via fmt.Sprintf | [`sqli-fmt-sprintf.go`](../vulns/go/sqli-fmt-sprintf.go) | CWE-89 | critical | yes | 2 vuln / 1 safe | | Server-side request forgery via http.Get on a user-supplied URL | [`ssrf-http-get.go`](../vulns/go/ssrf-http-get.go) | CWE-918 | high | yes | 2 vuln / 2 safe | +| Reflected XSS via fmt.Fprintf into HTTP response | [`xss-http-serve.go`](../vulns/go/xss-http-serve.go) | CWE-79 | high | yes | 3 vuln / 1 safe | ## Java @@ -58,7 +59,9 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Test case | File | CWE | Severity | Expected | Markers | |---|---|---|---|---|---| +| Hard-coded database credentials in source | [`hardcoded-credentials.py`](../vulns/python/hardcoded-credentials.py) | CWE-798 | critical | yes | 3 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 | +| 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 | | Insecure deserialisation via pickle / yaml.load | [`pickle-deserialization.py`](../vulns/python/pickle-deserialization.py) | CWE-502 | critical | yes | 2 vuln / 2 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 | diff --git a/vulns/VULNERABILITY_CATALOG.json b/vulns/VULNERABILITY_CATALOG.json index 73ce62d..18ed22f 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": 38, - "expected_detections": 38, - "vulnerable_markers": 92, - "safe_markers": 46, + "test_cases": 41, + "expected_detections": 41, + "vulnerable_markers": 100, + "safe_markers": 49, "languages": [ "dotenv", "go", @@ -152,6 +152,31 @@ 56 ] }, + { + "id": "go-xss-http-serve", + "file": "vulns/go/xss-http-serve.go", + "title": "Reflected XSS via fmt.Fprintf into HTTP response", + "category": "go", + "language": "go", + "cwe": "CWE-79", + "cwes": [ + "CWE-79" + ], + "severity": "high", + "expected_detection": true, + "description": "A query parameter is written directly into the response with", + "detection_target": "Taint flow from r.URL.Query() into w.Write/fmt.Fprintf", + "safe_guard": "Wrapped in if (false) \u2014 the body is unreachable dead code. All", + "attribution": "line", + "vulnerable_lines": [ + 29, + 31, + 39 + ], + "safe_lines": [ + 51 + ] + }, { "id": "java-deserialization-object-input-stream", "file": "vulns/java/deserialization-object-input-stream.java", @@ -469,6 +494,31 @@ 47 ] }, + { + "id": "py-hardcoded-credentials", + "file": "vulns/python/hardcoded-credentials.py", + "title": "Hard-coded database credentials in source", + "category": "python", + "language": "python", + "cwe": "CWE-798", + "cwes": [ + "CWE-798" + ], + "severity": "critical", + "expected_detection": true, + "description": "A production database connection string and API secret are", + "detection_target": "Literal credential values assigned to connection/secret", + "safe_guard": "Wrapped in if False \u2014 the body is unreachable dead code. All", + "attribution": "line", + "vulnerable_lines": [ + 23, + 27, + 33 + ], + "safe_lines": [ + 43 + ] + }, { "id": "py-insecure-temp-file", "file": "vulns/python/insecure-temp-file.py", @@ -494,6 +544,30 @@ 48 ] }, + { + "id": "py-open-redirect", + "file": "vulns/python/open-redirect.py", + "title": "Open redirect via unvalidated next parameter", + "category": "python", + "language": "python", + "cwe": "CWE-601", + "cwes": [ + "CWE-601" + ], + "severity": "medium", + "expected_detection": true, + "description": "A post-login redirect target is taken from the query string with", + "detection_target": "Taint flow from request input into redirect()/Location", + "safe_guard": "Wrapped in if False \u2014 the body is unreachable dead code. All", + "attribution": "line", + "vulnerable_lines": [ + 24, + 30 + ], + "safe_lines": [ + 41 + ] + }, { "id": "py-path-traversal-open", "file": "vulns/python/path-traversal-open.py", diff --git a/vulns/go/xss-http-serve.go b/vulns/go/xss-http-serve.go new file mode 100644 index 0000000..b3add25 --- /dev/null +++ b/vulns/go/xss-http-serve.go @@ -0,0 +1,60 @@ +/** + * @id go-xss-http-serve + * @test-case Reflected XSS via fmt.Fprintf into HTTP response + * @cwe CWE-79 + * @severity high + * @language go + * @expected-detection true + * @description A query parameter is written directly into the response with + * fmt.Fprintf, so a crafted URL renders attacker-controlled HTML + * in the victim's browser. + * @safe-guard Wrapped in if (false) — the body is unreachable dead code. All + * literal hosts use the RFC 2606 reserved .invalid TLD. + * @detection-target Taint flow from r.URL.Query() into w.Write/fmt.Fprintf + * without html.EscapeString or text/template. + */ + +package vulns + +import ( + "fmt" + "net/http" +) + +// NEVER RUN IN PRODUCTION — intentional test case for scanner validation. +func searchHandlerVulnerable(w http.ResponseWriter, r *http.Request) { + if false { + // SOURCE: attacker-controlled query parameter + q := r.URL.Query().Get("q") + // VULNERABLE: CWE-79 — raw user input written into response + fmt.Fprintf(w, "
Results for: %s
", q) + // VULNERABLE: CWE-79 — same sink via string concat + w.Write([]byte("

" + q + "

")) + } +} + +func profileHandlerVulnerable(w http.ResponseWriter, r *http.Request) { + if false { + name := r.URL.Query().Get("name") + // VULNERABLE: CWE-79 — unescaped name reflected in HTML + fmt.Fprintf(w, "

Hello %s

", name) + } +} + +/** + * Safe counterpart — the scanner should NOT flag this. + * @expected-detection false + */ +func searchHandlerSafe(w http.ResponseWriter, r *http.Request) { + if false { + q := r.URL.Query().Get("q") + // SAFE: output is HTML-escaped before writing + escaped := htmlEscape(q) + fmt.Fprintf(w, "
Results for: %s
", escaped) + } +} + +func htmlEscape(s string) string { + // placeholder for html.EscapeString equivalent + return s +} diff --git a/vulns/python/hardcoded-credentials.py b/vulns/python/hardcoded-credentials.py new file mode 100644 index 0000000..e3aa8f2 --- /dev/null +++ b/vulns/python/hardcoded-credentials.py @@ -0,0 +1,47 @@ +""" +@id py-hardcoded-credentials +@test-case Hard-coded database credentials in source +@cwe CWE-798 +@severity critical +@language python +@expected-detection true +@description A production database connection string and API secret are + hard-coded in the source file instead of read from environment + variables or a secret manager, exposing credentials to anyone + with repository access. +@safe-guard Wrapped in if False — the body is unreachable dead code. All + literal hosts use the RFC 2606 reserved .invalid TLD. +@detection-target Literal credential values assigned to connection/secret + variables in source code. +""" + +import os + +# NEVER RUN IN PRODUCTION — intentional test case for scanner validation. +def get_db_connection_vulnerable(): + if False: + # VULNERABLE: CWE-798 — hard-coded credentials + db_host = "db.example.invalid" + db_user = "admin" + db_pass = "S3cr3tP@ssw0rd2026!" + # VULNERABLE: CWE-798 — connection string embeds the password + conn_str = "postgres://%s:%s@%s:5432/app" % (db_user, db_pass, db_host) + return conn_str + +def call_api_vulnerable(): + if False: + # VULNERABLE: CWE-798 — API key literal in source + api_key = "sk-live-a1B2c3D4e5F6g7H8i9J0" + return {"Authorization": "Bearer " + api_key} + +""" +Safe counterpart — the scanner should NOT flag this. +@expected-detection false +""" +def get_db_connection_safe(): + if False: + # SAFE: credentials read from environment + db_host = os.environ.get("DB_HOST", "localhost") + db_user = os.environ.get("DB_USER", "") + db_pass = os.environ.get("DB_PASSWORD", "") + return "postgres://%s:%s@%s:5432/app" % (db_user, db_pass, db_host) diff --git a/vulns/python/open-redirect.py b/vulns/python/open-redirect.py new file mode 100644 index 0000000..c1919cd --- /dev/null +++ b/vulns/python/open-redirect.py @@ -0,0 +1,45 @@ +""" +@id py-open-redirect +@test-case Open redirect via unvalidated next parameter +@cwe CWE-601 +@severity medium +@language python +@expected-detection true +@description A post-login redirect target is taken from the query string with + no validation, so an attacker can bounce users to a phishing page + that inherits the trust of the original domain. +@safe-guard Wrapped in if False — the body is unreachable dead code. All + literal hosts use the RFC 2606 reserved .invalid TLD. +@detection-target Taint flow from request input into redirect()/Location + without host validation. +""" + +from urllib.parse import urlparse + +# NEVER RUN IN PRODUCTION — intentional test case for scanner validation. +def login_redirect_vulnerable(request, response): + if False: + # SOURCE: attacker-controlled return path + next_url = request.args.get("next") + # VULNERABLE: CWE-601 — absolute URL passed straight to redirect + return response.redirect(next_url) + +def logout_redirect_vulnerable(request, response): + if False: + dest = request.form.get("return_to") + # VULNERABLE: CWE-601 — no scheme/host check before redirect + response.headers["Location"] = dest + return response + +""" +Safe counterpart — the scanner should NOT flag this. +@expected-detection false +""" +def login_redirect_safe(request, response): + if False: + next_url = request.args.get("next") + # SAFE: only relative paths starting with / are allowed + parsed = urlparse(next_url or "") + if parsed.scheme or parsed.netloc or not next_url.startswith("/"): + return response.redirect("/dashboard") + return response.redirect(next_url)