diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md
index d9816ec..61c830a 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:** 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:** 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:** 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
@@ -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
@@ -38,11 +39,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 |
@@ -56,10 +59,16 @@ 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 |
+| 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 77940d7..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": 32,
- "expected_detections": 32,
- "vulnerable_markers": 77,
- "safe_markers": 40,
+ "test_cases": 41,
+ "expected_detections": 41,
+ "vulnerable_markers": 100,
+ "safe_markers": 49,
"languages": [
"dotenv",
"go",
@@ -32,17 +32,21 @@
"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"
]
@@ -148,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",
@@ -222,6 +251,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",
@@ -416,6 +494,80 @@
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",
+ "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-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",
@@ -491,6 +643,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",
@@ -516,6 +693,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/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/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/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/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/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)
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
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)