From d712c7aaf784758f643a5b043a7005679b998df5 Mon Sep 17 00:00:00 2001 From: walker Date: Tue, 25 Aug 2026 00:09:45 +0800 Subject: [PATCH] test: add 15 TypeScript security scanner fixtures --- docs/VULNERABILITY_CATALOG.md | 32 +- vulns/VULNERABILITY_CATALOG.json | 360 +++++++++++++++++- vulns/typescript/archive-path-traversal.ts | 28 ++ vulns/typescript/command-injection.ts | 25 ++ vulns/typescript/dom-xss.ts | 25 ++ vulns/typescript/eval-code-injection.ts | 22 ++ vulns/typescript/hardcoded-jwt-secret.ts | 24 ++ vulns/typescript/log-injection.ts | 24 ++ vulns/typescript/open-redirect.ts | 25 ++ vulns/typescript/partial-token-comparison.ts | 23 ++ vulns/typescript/path-traversal.ts | 23 ++ vulns/typescript/prototype-pollution.ts | 23 ++ vulns/typescript/regex-injection.ts | 21 + vulns/typescript/sql-injection.ts | 24 ++ vulns/typescript/ssrf-user-url.ts | 21 + .../typescript/unsafe-yaml-deserialization.ts | 23 ++ vulns/typescript/weak-random-token.ts | 23 ++ 17 files changed, 734 insertions(+), 12 deletions(-) create mode 100644 vulns/typescript/archive-path-traversal.ts create mode 100644 vulns/typescript/command-injection.ts create mode 100644 vulns/typescript/dom-xss.ts create mode 100644 vulns/typescript/eval-code-injection.ts create mode 100644 vulns/typescript/hardcoded-jwt-secret.ts create mode 100644 vulns/typescript/log-injection.ts create mode 100644 vulns/typescript/open-redirect.ts create mode 100644 vulns/typescript/partial-token-comparison.ts create mode 100644 vulns/typescript/path-traversal.ts create mode 100644 vulns/typescript/prototype-pollution.ts create mode 100644 vulns/typescript/regex-injection.ts create mode 100644 vulns/typescript/sql-injection.ts create mode 100644 vulns/typescript/ssrf-user-url.ts create mode 100644 vulns/typescript/unsafe-yaml-deserialization.ts create mode 100644 vulns/typescript/weak-random-token.ts diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index 01acd4c..470fa35 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:** 127 -- **Expected detections:** 127 -- **`VULNERABLE:` markers:** 236 (individual lines a scanner should flag) -- **`SAFE:` markers:** 144 (lines a scanner must not flag — the false-positive control group) -- **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text -- **CWE categories:** 89 — CWE-20, CWE-22, CWE-78, CWE-79, CWE-89, CWE-90, CWE-94, CWE-95, CWE-113, CWE-117, CWE-129, CWE-190, CWE-201, CWE-203, CWE-208, CWE-209, CWE-256, CWE-285, CWE-287, CWE-288, CWE-291, CWE-295, CWE-306, CWE-307, CWE-311, CWE-312, CWE-319, CWE-321, CWE-326, CWE-327, CWE-329, CWE-330, CWE-338, CWE-345, CWE-346, CWE-347, CWE-352, CWE-362, CWE-377, CWE-384, CWE-400, CWE-409, CWE-434, CWE-441, CWE-460, CWE-472, CWE-475, CWE-480, CWE-488, CWE-489, CWE-502, CWE-506, CWE-509, CWE-512, CWE-521, CWE-522, CWE-525, CWE-532, CWE-598, CWE-601, CWE-602, CWE-611, CWE-613, CWE-614, CWE-620, CWE-639, CWE-640, CWE-643, CWE-681, CWE-693, CWE-732, CWE-759, CWE-776, CWE-798, CWE-835, CWE-862, CWE-863, CWE-915, CWE-916, CWE-918, CWE-922, CWE-942, CWE-943, CWE-1021, CWE-1236, CWE-1321, CWE-1333, CWE-1336, CWE-1357 +- **Test cases:** 142 +- **Expected detections:** 142 +- **`VULNERABLE:` markers:** 251 (individual lines a scanner should flag) +- **`SAFE:` markers:** 159 (lines a scanner must not flag — the false-positive control group) +- **Languages:** 9 — dotenv, go, java, javascript, json, python, ruby, text, typescript +- **CWE categories:** 90 — CWE-20, CWE-22, CWE-78, CWE-79, CWE-89, CWE-90, CWE-94, CWE-95, CWE-113, CWE-117, CWE-129, CWE-187, CWE-190, CWE-201, CWE-203, CWE-208, CWE-209, CWE-256, CWE-285, CWE-287, CWE-288, CWE-291, CWE-295, CWE-306, CWE-307, CWE-311, CWE-312, CWE-319, CWE-321, CWE-326, CWE-327, CWE-329, CWE-330, CWE-338, CWE-345, CWE-346, CWE-347, CWE-352, CWE-362, CWE-377, CWE-384, CWE-400, CWE-409, CWE-434, CWE-441, CWE-460, CWE-472, CWE-475, CWE-480, CWE-488, CWE-489, CWE-502, CWE-506, CWE-509, CWE-512, CWE-521, CWE-522, CWE-525, CWE-532, CWE-598, CWE-601, CWE-602, CWE-611, CWE-613, CWE-614, CWE-620, CWE-639, CWE-640, CWE-643, CWE-681, CWE-693, CWE-732, CWE-759, CWE-776, CWE-798, CWE-835, CWE-862, CWE-863, CWE-915, CWE-916, CWE-918, CWE-922, CWE-942, CWE-943, CWE-1021, CWE-1236, CWE-1321, CWE-1333, CWE-1336, CWE-1357 ## How coverage is scored @@ -184,6 +184,26 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Typosquatted and dependency-confusion entries in package.json | [`package.json`](../vulns/supply-chain/package.json) | CWE-1357 | critical | yes | file-level / 0 safe | | Typosquatted and dependency-confusion entries in requirements.txt | [`requirements.txt`](../vulns/supply-chain/requirements.txt) | CWE-1357 | critical | yes | 4 vuln / 1 safe | +## Typescript + +| Test case | File | CWE | Severity | Expected | Markers | +|---|---|---|---|---|---| +| Archive extraction path traversal | [`archive-path-traversal.ts`](../vulns/typescript/archive-path-traversal.ts) | CWE-22 | high | yes | 1 vuln / 1 safe | +| Shell command injection through exec | [`command-injection.ts`](../vulns/typescript/command-injection.ts) | CWE-78 | critical | yes | 1 vuln / 1 safe | +| DOM cross-site scripting through innerHTML | [`dom-xss.ts`](../vulns/typescript/dom-xss.ts) | CWE-79 | high | yes | 1 vuln / 1 safe | +| Dynamic code execution from request input | [`eval-code-injection.ts`](../vulns/typescript/eval-code-injection.ts) | CWE-95 | critical | yes | 1 vuln / 1 safe | +| Hard-coded JWT signing secret | [`hardcoded-jwt-secret.ts`](../vulns/typescript/hardcoded-jwt-secret.ts) | CWE-798 | high | yes | 1 vuln / 1 safe | +| Log injection through unsanitized username | [`log-injection.ts`](../vulns/typescript/log-injection.ts) | CWE-117 | medium | yes | 1 vuln / 1 safe | +| Open redirect through untrusted destination | [`open-redirect.ts`](../vulns/typescript/open-redirect.ts) | CWE-601 | medium | yes | 1 vuln / 1 safe | +| Authentication using a truncated token comparison | [`partial-token-comparison.ts`](../vulns/typescript/partial-token-comparison.ts) | CWE-187 | high | yes | 1 vuln / 1 safe | +| Path traversal in file download | [`path-traversal.ts`](../vulns/typescript/path-traversal.ts) | CWE-22 | high | yes | 1 vuln / 1 safe | +| Prototype pollution through recursive merge | [`prototype-pollution.ts`](../vulns/typescript/prototype-pollution.ts) | CWE-1321 | high | yes | 1 vuln / 1 safe | +| Regular expression denial of service from user pattern | [`regex-injection.ts`](../vulns/typescript/regex-injection.ts) | CWE-1333 | high | yes | 1 vuln / 1 safe | +| SQL injection through template interpolation | [`sql-injection.ts`](../vulns/typescript/sql-injection.ts) | CWE-89 | critical | yes | 1 vuln / 1 safe | +| Server-side request forgery through user URL | [`ssrf-user-url.ts`](../vulns/typescript/ssrf-user-url.ts) | CWE-918 | high | yes | 1 vuln / 1 safe | +| Unsafe deserialization of tagged YAML | [`unsafe-yaml-deserialization.ts`](../vulns/typescript/unsafe-yaml-deserialization.ts) | CWE-502 | critical | yes | 1 vuln / 1 safe | +| Predictable session token from Math.random | [`weak-random-token.ts`](../vulns/typescript/weak-random-token.ts) | CWE-338 | high | yes | 1 vuln / 1 safe | + ## Safety Every payload above is unreachable dead code, every host literal uses an diff --git a/vulns/VULNERABILITY_CATALOG.json b/vulns/VULNERABILITY_CATALOG.json index 8fb4128..bb7c398 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": 127, - "expected_detections": 127, - "vulnerable_markers": 236, - "safe_markers": 144, + "test_cases": 142, + "expected_detections": 142, + "vulnerable_markers": 251, + "safe_markers": 159, "languages": [ "dotenv", "go", @@ -14,7 +14,8 @@ "json", "python", "ruby", - "text" + "text", + "typescript" ], "categories": [ "go", @@ -23,7 +24,8 @@ "python", "ruby", "secrets", - "supply-chain" + "supply-chain", + "typescript" ], "cwes": [ "CWE-20", @@ -37,6 +39,7 @@ "CWE-113", "CWE-117", "CWE-129", + "CWE-187", "CWE-190", "CWE-201", "CWE-203", @@ -3164,6 +3167,351 @@ "safe_lines": [ 37 ] + }, + { + "id": "ts-archive-path-traversal", + "file": "vulns/typescript/archive-path-traversal.ts", + "title": "Archive extraction path traversal", + "category": "typescript", + "language": "typescript", + "cwe": "CWE-22", + "cwes": [ + "CWE-22" + ], + "severity": "high", + "expected_detection": true, + "description": "An archive entry name is appended to the extraction root unchecked.", + "detection_target": "Untrusted archive filename used as an output path.", + "safe_guard": "NEVER_RUN is false, so no archive entry is written.", + "attribution": "line", + "vulnerable_lines": [ + 23 + ], + "safe_lines": [ + 26 + ] + }, + { + "id": "ts-command-injection", + "file": "vulns/typescript/command-injection.ts", + "title": "Shell command injection through exec", + "category": "typescript", + "language": "typescript", + "cwe": "CWE-78", + "cwes": [ + "CWE-78" + ], + "severity": "critical", + "expected_detection": true, + "description": "User input is interpolated into a shell command.", + "detection_target": "Tainted data passed to a shell execution sink.", + "safe_guard": "NEVER_RUN is false, so no process starts.", + "attribution": "line", + "vulnerable_lines": [ + 19 + ], + "safe_lines": [ + 22 + ] + }, + { + "id": "ts-dom-xss", + "file": "vulns/typescript/dom-xss.ts", + "title": "DOM cross-site scripting through innerHTML", + "category": "typescript", + "language": "typescript", + "cwe": "CWE-79", + "cwes": [ + "CWE-79" + ], + "severity": "high", + "expected_detection": true, + "description": "Request text is assigned directly to an HTML sink.", + "detection_target": "Tainted data assigned to innerHTML.", + "safe_guard": "NEVER_RUN is false, so no DOM mutation occurs.", + "attribution": "line", + "vulnerable_lines": [ + 21 + ], + "safe_lines": [ + 23 + ] + }, + { + "id": "ts-eval-code-injection", + "file": "vulns/typescript/eval-code-injection.ts", + "title": "Dynamic code execution from request input", + "category": "typescript", + "language": "typescript", + "cwe": "CWE-95", + "cwes": [ + "CWE-95" + ], + "severity": "critical", + "expected_detection": true, + "description": "Attacker-controlled text is evaluated as JavaScript.", + "detection_target": "Tainted input passed to eval.", + "safe_guard": "NEVER_RUN is false, so no expression executes.", + "attribution": "line", + "vulnerable_lines": [ + 16 + ], + "safe_lines": [ + 21 + ] + }, + { + "id": "ts-hardcoded-jwt-secret", + "file": "vulns/typescript/hardcoded-jwt-secret.ts", + "title": "Hard-coded JWT signing secret", + "category": "typescript", + "language": "typescript", + "cwe": "CWE-798", + "cwes": [ + "CWE-798" + ], + "severity": "high", + "expected_detection": true, + "description": "A synthetic signing secret is embedded directly in source.", + "detection_target": "Secret argument sourced from a string literal.", + "safe_guard": "NEVER_RUN is false and the value signs no token.", + "attribution": "line", + "vulnerable_lines": [ + 19 + ], + "safe_lines": [ + 22 + ] + }, + { + "id": "ts-log-injection", + "file": "vulns/typescript/log-injection.ts", + "title": "Log injection through unsanitized username", + "category": "typescript", + "language": "typescript", + "cwe": "CWE-117", + "cwes": [ + "CWE-117" + ], + "severity": "medium", + "expected_detection": true, + "description": "Request text is written directly into a structured log line.", + "detection_target": "Tainted text reaches a logging sink without neutralization.", + "safe_guard": "NEVER_RUN is false, so no log entry is emitted.", + "attribution": "line", + "vulnerable_lines": [ + 20 + ], + "safe_lines": [ + 22 + ] + }, + { + "id": "ts-open-redirect", + "file": "vulns/typescript/open-redirect.ts", + "title": "Open redirect through untrusted destination", + "category": "typescript", + "language": "typescript", + "cwe": "CWE-601", + "cwes": [ + "CWE-601" + ], + "severity": "medium", + "expected_detection": true, + "description": "A request destination is passed directly to a redirect sink.", + "detection_target": "Tainted URL passed to response.redirect.", + "safe_guard": "NEVER_RUN is false, so no redirect response is emitted.", + "attribution": "line", + "vulnerable_lines": [ + 20 + ], + "safe_lines": [ + 23 + ] + }, + { + "id": "ts-partial-token-comparison", + "file": "vulns/typescript/partial-token-comparison.ts", + "title": "Authentication using a truncated token comparison", + "category": "typescript", + "language": "typescript", + "cwe": "CWE-187", + "cwes": [ + "CWE-187" + ], + "severity": "high", + "expected_detection": true, + "description": "Only the first eight characters of a bearer token are compared.", + "detection_target": "slice used to compare partial credentials.", + "safe_guard": "NEVER_RUN is false, so no access decision occurs.", + "attribution": "line", + "vulnerable_lines": [ + 19 + ], + "safe_lines": [ + 21 + ] + }, + { + "id": "ts-path-traversal", + "file": "vulns/typescript/path-traversal.ts", + "title": "Path traversal in file download", + "category": "typescript", + "language": "typescript", + "cwe": "CWE-22", + "cwes": [ + "CWE-22" + ], + "severity": "high", + "expected_detection": true, + "description": "A request filename is joined to a trusted directory unchecked.", + "detection_target": "Tainted path passed to a file read sink.", + "safe_guard": "NEVER_RUN is false, so no file is read.", + "attribution": "line", + "vulnerable_lines": [ + 18 + ], + "safe_lines": [ + 21 + ] + }, + { + "id": "ts-prototype-pollution", + "file": "vulns/typescript/prototype-pollution.ts", + "title": "Prototype pollution through recursive merge", + "category": "typescript", + "language": "typescript", + "cwe": "CWE-1321", + "cwes": [ + "CWE-1321" + ], + "severity": "high", + "expected_detection": true, + "description": "Untrusted object keys are copied into an application configuration.", + "detection_target": "Untrusted keys assigned without blocking prototype properties.", + "safe_guard": "NEVER_RUN is false, so no object is mutated.", + "attribution": "line", + "vulnerable_lines": [ + 16 + ], + "safe_lines": [ + 20 + ] + }, + { + "id": "ts-regex-injection", + "file": "vulns/typescript/regex-injection.ts", + "title": "Regular expression denial of service from user pattern", + "category": "typescript", + "language": "typescript", + "cwe": "CWE-1333", + "cwes": [ + "CWE-1333" + ], + "severity": "high", + "expected_detection": true, + "description": "An attacker-controlled regular expression is applied to large input.", + "detection_target": "Tainted string passed to RegExp.", + "safe_guard": "NEVER_RUN is false, so no pattern is compiled.", + "attribution": "line", + "vulnerable_lines": [ + 16 + ], + "safe_lines": [ + 20 + ] + }, + { + "id": "ts-sql-injection", + "file": "vulns/typescript/sql-injection.ts", + "title": "SQL injection through template interpolation", + "category": "typescript", + "language": "typescript", + "cwe": "CWE-89", + "cwes": [ + "CWE-89" + ], + "severity": "critical", + "expected_detection": true, + "description": "User input is embedded directly into SQL query text.", + "detection_target": "Tainted data interpolated into a database query.", + "safe_guard": "NEVER_RUN is false, so no query executes.", + "attribution": "line", + "vulnerable_lines": [ + 20 + ], + "safe_lines": [ + 22 + ] + }, + { + "id": "ts-ssrf-user-url", + "file": "vulns/typescript/ssrf-user-url.ts", + "title": "Server-side request forgery through user URL", + "category": "typescript", + "language": "typescript", + "cwe": "CWE-918", + "cwes": [ + "CWE-918" + ], + "severity": "high", + "expected_detection": true, + "description": "A user-provided URL is fetched without host validation.", + "detection_target": "Tainted URL passed to fetch.", + "safe_guard": "NEVER_RUN is false, so no network request occurs.", + "attribution": "line", + "vulnerable_lines": [ + 16 + ], + "safe_lines": [ + 19 + ] + }, + { + "id": "ts-unsafe-yaml-deserialization", + "file": "vulns/typescript/unsafe-yaml-deserialization.ts", + "title": "Unsafe deserialization of tagged YAML", + "category": "typescript", + "language": "typescript", + "cwe": "CWE-502", + "cwes": [ + "CWE-502" + ], + "severity": "critical", + "expected_detection": true, + "description": "Untrusted YAML is decoded with support for arbitrary object tags.", + "detection_target": "Untrusted text passed to an unsafe YAML loader.", + "safe_guard": "NEVER_RUN is false, so no payload is decoded.", + "attribution": "line", + "vulnerable_lines": [ + 19 + ], + "safe_lines": [ + 21 + ] + }, + { + "id": "ts-weak-random-token", + "file": "vulns/typescript/weak-random-token.ts", + "title": "Predictable session token from Math.random", + "category": "typescript", + "language": "typescript", + "cwe": "CWE-338", + "cwes": [ + "CWE-338" + ], + "severity": "high", + "expected_detection": true, + "description": "A session token is generated with a non-cryptographic PRNG.", + "detection_target": "Math.random used for authentication material.", + "safe_guard": "NEVER_RUN is false, so no token is issued.", + "attribution": "line", + "vulnerable_lines": [ + 19 + ], + "safe_lines": [ + 21 + ] } ] } diff --git a/vulns/typescript/archive-path-traversal.ts b/vulns/typescript/archive-path-traversal.ts new file mode 100644 index 0000000..078cc5d --- /dev/null +++ b/vulns/typescript/archive-path-traversal.ts @@ -0,0 +1,28 @@ +// @id ts-archive-path-traversal +// @test-case Archive extraction path traversal +// @cwe CWE-22 +// @severity high +// @language typescript +// @expected-detection true +// @description An archive entry name is appended to the extraction root unchecked. +// @safe-guard NEVER_RUN is false, so no archive entry is written. +// @detection-target Untrusted archive filename used as an output path. +// NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +const NEVER_RUN = false; + +declare function writeFile(path: string, bytes: Uint8Array): Promise; + +interface ArchiveEntry { + name: string; + bytes: Uint8Array; +} + +export async function fixture(entry: ArchiveEntry): Promise { + if (NEVER_RUN) { + await writeFile(`/srv/extract/${entry.name}`, entry.bytes); // VULNERABLE: CWE-22 + + const safeName = entry.name.match(/^[\w.-]+$/)?.[0]; + if (safeName) await writeFile(`/srv/extract/${safeName}`, entry.bytes); // SAFE: basename allow-list + } +} diff --git a/vulns/typescript/command-injection.ts b/vulns/typescript/command-injection.ts new file mode 100644 index 0000000..09897af --- /dev/null +++ b/vulns/typescript/command-injection.ts @@ -0,0 +1,25 @@ +// @id ts-command-injection +// @test-case Shell command injection through exec +// @cwe CWE-78 +// @severity critical +// @language typescript +// @expected-detection true +// @description User input is interpolated into a shell command. +// @safe-guard NEVER_RUN is false, so no process starts. +// @detection-target Tainted data passed to a shell execution sink. +// NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +const NEVER_RUN = false; + +declare function exec(command: string): Promise; +declare function execFile(program: string, arguments_: readonly string[]): Promise; + +export async function fixture(filename: string): Promise { + if (NEVER_RUN) { + await exec(`cat ${filename}`); // VULNERABLE: CWE-78 + + if (/^[\w.-]+$/.test(filename)) { + await execFile('cat', ['--', `/srv/reports/${filename}`]); // SAFE: no shell + } + } +} diff --git a/vulns/typescript/dom-xss.ts b/vulns/typescript/dom-xss.ts new file mode 100644 index 0000000..d0f1602 --- /dev/null +++ b/vulns/typescript/dom-xss.ts @@ -0,0 +1,25 @@ +// @id ts-dom-xss +// @test-case DOM cross-site scripting through innerHTML +// @cwe CWE-79 +// @severity high +// @language typescript +// @expected-detection true +// @description Request text is assigned directly to an HTML sink. +// @safe-guard NEVER_RUN is false, so no DOM mutation occurs. +// @detection-target Tainted data assigned to innerHTML. +// NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +const NEVER_RUN = false; + +interface ElementLike { + innerHTML: string; + textContent: string | null; +} + +export function fixture(element: ElementLike, message: string): void { + if (NEVER_RUN) { + element.innerHTML = message; // VULNERABLE: CWE-79 + + element.textContent = message; // SAFE: text-only sink + } +} diff --git a/vulns/typescript/eval-code-injection.ts b/vulns/typescript/eval-code-injection.ts new file mode 100644 index 0000000..0a20655 --- /dev/null +++ b/vulns/typescript/eval-code-injection.ts @@ -0,0 +1,22 @@ +// @id ts-eval-code-injection +// @test-case Dynamic code execution from request input +// @cwe CWE-95 +// @severity critical +// @language typescript +// @expected-detection true +// @description Attacker-controlled text is evaluated as JavaScript. +// @safe-guard NEVER_RUN is false, so no expression executes. +// @detection-target Tainted input passed to eval. +// NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +const NEVER_RUN = false; + +export function fixture(expression: string): unknown { + if (NEVER_RUN) { + const result: unknown = eval(expression); // VULNERABLE: CWE-95 + return result; + } + + const allowed: Record string> = { status: () => 'ok' }; + return allowed[expression]?.(); // SAFE: allow-listed callback +} diff --git a/vulns/typescript/hardcoded-jwt-secret.ts b/vulns/typescript/hardcoded-jwt-secret.ts new file mode 100644 index 0000000..fd527b1 --- /dev/null +++ b/vulns/typescript/hardcoded-jwt-secret.ts @@ -0,0 +1,24 @@ +// @id ts-hardcoded-jwt-secret +// @test-case Hard-coded JWT signing secret +// @cwe CWE-798 +// @severity high +// @language typescript +// @expected-detection true +// @description A synthetic signing secret is embedded directly in source. +// @safe-guard NEVER_RUN is false and the value signs no token. +// @detection-target Secret argument sourced from a string literal. +// NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +const NEVER_RUN = false; + +declare const environment: Record; +declare function signToken(payload: object, secret: string): string; + +export function fixture(payload: object): void { + if (NEVER_RUN) { + signToken(payload, 'TEST_ONLY_jwt_secret_123'); // VULNERABLE: CWE-798 + + const secret = environment.JWT_SECRET; + if (secret) signToken(payload, secret); // SAFE: external secret source + } +} diff --git a/vulns/typescript/log-injection.ts b/vulns/typescript/log-injection.ts new file mode 100644 index 0000000..14976b8 --- /dev/null +++ b/vulns/typescript/log-injection.ts @@ -0,0 +1,24 @@ +// @id ts-log-injection +// @test-case Log injection through unsanitized username +// @cwe CWE-117 +// @severity medium +// @language typescript +// @expected-detection true +// @description Request text is written directly into a structured log line. +// @safe-guard NEVER_RUN is false, so no log entry is emitted. +// @detection-target Tainted text reaches a logging sink without neutralization. +// NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +const NEVER_RUN = false; + +interface Logger { + info(message: string): void; +} + +export function fixture(logger: Logger, username: string): void { + if (NEVER_RUN) { + logger.info(`login failed user=${username}`); // VULNERABLE: CWE-117 + + logger.info(`login failed user=${username.replace(/[\r\n]/g, '_')}`); // SAFE: line breaks neutralized + } +} diff --git a/vulns/typescript/open-redirect.ts b/vulns/typescript/open-redirect.ts new file mode 100644 index 0000000..520b7e7 --- /dev/null +++ b/vulns/typescript/open-redirect.ts @@ -0,0 +1,25 @@ +// @id ts-open-redirect +// @test-case Open redirect through untrusted destination +// @cwe CWE-601 +// @severity medium +// @language typescript +// @expected-detection true +// @description A request destination is passed directly to a redirect sink. +// @safe-guard NEVER_RUN is false, so no redirect response is emitted. +// @detection-target Tainted URL passed to response.redirect. +// NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +const NEVER_RUN = false; + +interface Response { + redirect(location: string): void; +} + +export function fixture(response: Response, next: string): void { + if (NEVER_RUN) { + response.redirect(next); // VULNERABLE: CWE-601 + + const routes: Record = { home: '/', account: '/account' }; + response.redirect(routes[next] ?? '/'); // SAFE: internal allow-list + } +} diff --git a/vulns/typescript/partial-token-comparison.ts b/vulns/typescript/partial-token-comparison.ts new file mode 100644 index 0000000..d7993fa --- /dev/null +++ b/vulns/typescript/partial-token-comparison.ts @@ -0,0 +1,23 @@ +// @id ts-partial-token-comparison +// @test-case Authentication using a truncated token comparison +// @cwe CWE-187 +// @severity high +// @language typescript +// @expected-detection true +// @description Only the first eight characters of a bearer token are compared. +// @safe-guard NEVER_RUN is false, so no access decision occurs. +// @detection-target slice used to compare partial credentials. +// NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +const NEVER_RUN = false; + +declare function constantTimeEqual(left: string, right: string): boolean; +declare function grantAccess(): void; + +export function fixture(provided: string, expected: string): void { + if (NEVER_RUN) { + if (provided.slice(0, 8) === expected.slice(0, 8)) grantAccess(); // VULNERABLE: CWE-187 + + if (constantTimeEqual(provided, expected)) grantAccess(); // SAFE: full comparison + } +} diff --git a/vulns/typescript/path-traversal.ts b/vulns/typescript/path-traversal.ts new file mode 100644 index 0000000..43cb70a --- /dev/null +++ b/vulns/typescript/path-traversal.ts @@ -0,0 +1,23 @@ +// @id ts-path-traversal +// @test-case Path traversal in file download +// @cwe CWE-22 +// @severity high +// @language typescript +// @expected-detection true +// @description A request filename is joined to a trusted directory unchecked. +// @safe-guard NEVER_RUN is false, so no file is read. +// @detection-target Tainted path passed to a file read sink. +// NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +const NEVER_RUN = false; + +declare function readFile(path: string): Promise; + +export async function fixture(name: string): Promise { + if (NEVER_RUN) { + await readFile(`/srv/public/${name}`); // VULNERABLE: CWE-22 + + const safeName = name.match(/^[\w.-]+$/)?.[0]; + if (safeName) await readFile(`/srv/public/${safeName}`); // SAFE: basename allow-list + } +} diff --git a/vulns/typescript/prototype-pollution.ts b/vulns/typescript/prototype-pollution.ts new file mode 100644 index 0000000..5cec3e0 --- /dev/null +++ b/vulns/typescript/prototype-pollution.ts @@ -0,0 +1,23 @@ +// @id ts-prototype-pollution +// @test-case Prototype pollution through recursive merge +// @cwe CWE-1321 +// @severity high +// @language typescript +// @expected-detection true +// @description Untrusted object keys are copied into an application configuration. +// @safe-guard NEVER_RUN is false, so no object is mutated. +// @detection-target Untrusted keys assigned without blocking prototype properties. +// NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +const NEVER_RUN = false; + +export function fixture(config: Record, body: string): void { + if (NEVER_RUN) { + Object.assign(config, JSON.parse(body)); // VULNERABLE: CWE-1321 + + const input = JSON.parse(body) as Record; + for (const [key, value] of Object.entries(input)) { + if (!['__proto__', 'prototype', 'constructor'].includes(key)) config[key] = value; // SAFE: prototype keys blocked + } + } +} diff --git a/vulns/typescript/regex-injection.ts b/vulns/typescript/regex-injection.ts new file mode 100644 index 0000000..001205b --- /dev/null +++ b/vulns/typescript/regex-injection.ts @@ -0,0 +1,21 @@ +// @id ts-regex-injection +// @test-case Regular expression denial of service from user pattern +// @cwe CWE-1333 +// @severity high +// @language typescript +// @expected-detection true +// @description An attacker-controlled regular expression is applied to large input. +// @safe-guard NEVER_RUN is false, so no pattern is compiled. +// @detection-target Tainted string passed to RegExp. +// NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +const NEVER_RUN = false; + +export function fixture(pattern: string, text: string): boolean { + if (NEVER_RUN) { + return new RegExp(pattern).test(text); // VULNERABLE: CWE-1333 + } + + const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return new RegExp(escaped).test(text.slice(0, 4096)); // SAFE: escaped and bounded +} diff --git a/vulns/typescript/sql-injection.ts b/vulns/typescript/sql-injection.ts new file mode 100644 index 0000000..99e8adc --- /dev/null +++ b/vulns/typescript/sql-injection.ts @@ -0,0 +1,24 @@ +// @id ts-sql-injection +// @test-case SQL injection through template interpolation +// @cwe CWE-89 +// @severity critical +// @language typescript +// @expected-detection true +// @description User input is embedded directly into SQL query text. +// @safe-guard NEVER_RUN is false, so no query executes. +// @detection-target Tainted data interpolated into a database query. +// NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +const NEVER_RUN = false; + +interface Database { + query(sql: string, parameters?: readonly unknown[]): Promise; +} + +export async function fixture(db: Database, email: string): Promise { + if (NEVER_RUN) { + await db.query(`SELECT * FROM users WHERE email = '${email}'`); // VULNERABLE: CWE-89 + + await db.query('SELECT * FROM users WHERE email = ?', [email]); // SAFE: bound parameter + } +} diff --git a/vulns/typescript/ssrf-user-url.ts b/vulns/typescript/ssrf-user-url.ts new file mode 100644 index 0000000..86df01d --- /dev/null +++ b/vulns/typescript/ssrf-user-url.ts @@ -0,0 +1,21 @@ +// @id ts-ssrf-user-url +// @test-case Server-side request forgery through user URL +// @cwe CWE-918 +// @severity high +// @language typescript +// @expected-detection true +// @description A user-provided URL is fetched without host validation. +// @safe-guard NEVER_RUN is false, so no network request occurs. +// @detection-target Tainted URL passed to fetch. +// NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +const NEVER_RUN = false; + +export async function fixture(target: string): Promise { + if (NEVER_RUN) { + await fetch(target); // VULNERABLE: CWE-918 + + const reportId = target.match(/^report-(\d+)$/)?.[1]; + if (reportId) await fetch(`https://reports.invalid/${reportId}`); // SAFE: fixed .invalid host + } +} diff --git a/vulns/typescript/unsafe-yaml-deserialization.ts b/vulns/typescript/unsafe-yaml-deserialization.ts new file mode 100644 index 0000000..32d03d4 --- /dev/null +++ b/vulns/typescript/unsafe-yaml-deserialization.ts @@ -0,0 +1,23 @@ +// @id ts-unsafe-yaml-deserialization +// @test-case Unsafe deserialization of tagged YAML +// @cwe CWE-502 +// @severity critical +// @language typescript +// @expected-detection true +// @description Untrusted YAML is decoded with support for arbitrary object tags. +// @safe-guard NEVER_RUN is false, so no payload is decoded. +// @detection-target Untrusted text passed to an unsafe YAML loader. +// NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +const NEVER_RUN = false; + +declare function unsafeYamlLoad(input: string): unknown; +declare function consume(value: unknown): void; + +export function fixture(body: string): void { + if (NEVER_RUN) { + consume(unsafeYamlLoad(body)); // VULNERABLE: CWE-502 + + consume(JSON.parse(body)); // SAFE: data-only JSON parser + } +} diff --git a/vulns/typescript/weak-random-token.ts b/vulns/typescript/weak-random-token.ts new file mode 100644 index 0000000..d2f2b4c --- /dev/null +++ b/vulns/typescript/weak-random-token.ts @@ -0,0 +1,23 @@ +// @id ts-weak-random-token +// @test-case Predictable session token from Math.random +// @cwe CWE-338 +// @severity high +// @language typescript +// @expected-detection true +// @description A session token is generated with a non-cryptographic PRNG. +// @safe-guard NEVER_RUN is false, so no token is issued. +// @detection-target Math.random used for authentication material. +// NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +const NEVER_RUN = false; + +declare function randomBytes(length: number): Uint8Array; +declare function issueSession(token: string | Uint8Array): void; + +export function fixture(): void { + if (NEVER_RUN) { + issueSession(Math.random().toString(36)); // VULNERABLE: CWE-338 + + issueSession(randomBytes(32)); // SAFE: cryptographic random bytes + } +}