diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index be2ab1c..609cd2c 100644 --- a/docs/VULNERABILITY_CATALOG.md +++ b/docs/VULNERABILITY_CATALOG.md @@ -7,11 +7,11 @@ from each file's header comment, so this page cannot drift from the source. ## Totals -- **Test cases:** 128 -- **Expected detections:** 128 -- **`VULNERABLE:` markers:** 237 (individual lines a scanner should flag) -- **`SAFE:` markers:** 145 (lines a scanner must not flag — the false-positive control group) -- **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text +- **Test cases:** 143 +- **Expected detections:** 143 +- **`VULNERABLE:` markers:** 253 (individual lines a scanner should flag) +- **`SAFE:` markers:** 160 (lines a scanner must not flag — the false-positive control group) +- **Languages:** 9 — dotenv, go, java, javascript, json, python, ruby, rust, text - **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-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-1004, CWE-1021, CWE-1236, CWE-1321, CWE-1333, CWE-1336, CWE-1357 ## How coverage is scored @@ -167,6 +167,26 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | SQL injection via string interpolation in ActiveRecord | [`sqli-string-interpolation.rb`](../vulns/ruby/sqli-string-interpolation.rb) | CWE-89 | critical | yes | 3 vuln / 1 safe | | XSS via html_safe / raw on untrusted input | [`xss-erb-html-safe.rb`](../vulns/ruby/xss-erb-html-safe.rb) | CWE-79 | high | yes | 3 vuln / 1 safe | +## Rust + +| Test case | File | CWE | Severity | Expected | Markers | +|---|---|---|---|---|---| +| State-changing request accepted without CSRF verification | [`csrf-missing-token.rs`](../vulns/rust/csrf-missing-token.rs) | CWE-352 | high | yes | 1 vuln / 1 safe | +| Hard-coded encryption key embedded in source | [`hardcoded-crypto-key.rs`](../vulns/rust/hardcoded-crypto-key.rs) | CWE-321 | high | yes | 1 vuln / 1 safe | +| HTTP response splitting through a tainted header value | [`header-injection.rs`](../vulns/rust/header-injection.rs) | CWE-113 | high | yes | 1 vuln / 1 safe | +| IDOR through an unscoped record lookup | [`idor-unscoped-record.rs`](../vulns/rust/idor-unscoped-record.rs) | CWE-639 | high | yes | 1 vuln / 1 safe | +| Predictable recovery token derived from system time | [`insecure-random-token.rs`](../vulns/rust/insecure-random-token.rs) | CWE-330 | high | yes | 1 vuln / 1 safe | +| JWT claims trusted without signature verification | [`jwt-decode-without-verify.rs`](../vulns/rust/jwt-decode-without-verify.rs) | CWE-347 | critical | yes | 1 vuln / 1 safe | +| LDAP filter injection through string interpolation | [`ldap-filter-injection.rs`](../vulns/rust/ldap-filter-injection.rs) | CWE-90 | high | yes | 1 vuln / 1 safe | +| Log injection through unsanitized request data | [`log-injection.rs`](../vulns/rust/log-injection.rs) | CWE-117 | medium | yes | 1 vuln / 1 safe | +| Mass assignment of untrusted account attributes | [`mass-assignment.rs`](../vulns/rust/mass-assignment.rs) | CWE-915 | high | yes | 1 vuln / 1 safe | +| Open redirect via an unvalidated return URL | [`open-redirect.rs`](../vulns/rust/open-redirect.rs) | CWE-601 | medium | yes | 1 vuln / 1 safe | +| Credentialed CORS response reflects an arbitrary origin | [`permissive-cors.rs`](../vulns/rust/permissive-cors.rs) | CWE-942 | high | yes | 2 vuln / 1 safe | +| ReDoS through attacker-controlled pattern compilation | [`regex-dos-user-pattern.rs`](../vulns/rust/regex-dos-user-pattern.rs) | CWE-1333 | medium | yes | 1 vuln / 1 safe | +| Unsafe polymorphic deserialization of untrusted bytes | [`unsafe-polymorphic-deserialization.rs`](../vulns/rust/unsafe-polymorphic-deserialization.rs) | CWE-502 | critical | yes | 1 vuln / 1 safe | +| Password storage with a non-cryptographic hash | [`weak-password-hash.rs`](../vulns/rust/weak-password-hash.rs) | CWE-916 | high | yes | 1 vuln / 1 safe | +| XML parser resolves attacker-controlled external entities | [`xxe-parser.rs`](../vulns/rust/xxe-parser.rs) | CWE-611 | high | yes | 1 vuln / 1 safe | + ## Secrets & Credentials | Test case | File | CWE | Severity | Expected | Markers | diff --git a/vulns/VULNERABILITY_CATALOG.json b/vulns/VULNERABILITY_CATALOG.json index 0028a4d..6a188e9 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": 128, - "expected_detections": 128, - "vulnerable_markers": 237, - "safe_markers": 145, + "test_cases": 143, + "expected_detections": 143, + "vulnerable_markers": 253, + "safe_markers": 160, "languages": [ "dotenv", "go", @@ -14,6 +14,7 @@ "json", "python", "ruby", + "rust", "text" ], "categories": [ @@ -22,6 +23,7 @@ "javascript", "python", "ruby", + "rust", "secrets", "supply-chain" ], @@ -2996,6 +2998,352 @@ 44 ] }, + { + "id": "rust-csrf-missing-token", + "file": "vulns/rust/csrf-missing-token.rs", + "title": "State-changing request accepted without CSRF verification", + "category": "rust", + "language": "rust", + "cwe": "CWE-352", + "cwes": [ + "CWE-352" + ], + "severity": "high", + "expected_detection": true, + "description": "A cookie-authenticated mutation runs without a CSRF token check.", + "detection_target": "State changes without synchronizer-token validation.", + "safe_guard": "Every example is behind `NEVER_RUN`; no mutation occurs.", + "attribution": "line", + "vulnerable_lines": [ + 20 + ], + "safe_lines": [ + 26 + ] + }, + { + "id": "rust-hardcoded-crypto-key", + "file": "vulns/rust/hardcoded-crypto-key.rs", + "title": "Hard-coded encryption key embedded in source", + "category": "rust", + "language": "rust", + "cwe": "CWE-321", + "cwes": [ + "CWE-321" + ], + "severity": "high", + "expected_detection": true, + "description": "Encryption key material is stored as a source-code literal.", + "detection_target": "Static key literals used as cryptographic secrets.", + "safe_guard": "Every example is behind `NEVER_RUN`; the literal is synthetic.", + "attribution": "line", + "vulnerable_lines": [ + 18 + ], + "safe_lines": [ + 25 + ] + }, + { + "id": "rust-header-injection", + "file": "vulns/rust/header-injection.rs", + "title": "HTTP response splitting through a tainted header value", + "category": "rust", + "language": "rust", + "cwe": "CWE-113", + "cwes": [ + "CWE-113" + ], + "severity": "high", + "expected_detection": true, + "description": "Request text is concatenated into a response header.", + "detection_target": "CRLF-capable input used in response headers.", + "safe_guard": "Every example is behind `NEVER_RUN`; no header is emitted.", + "attribution": "line", + "vulnerable_lines": [ + 18 + ], + "safe_lines": [ + 26 + ] + }, + { + "id": "rust-idor-unscoped-record", + "file": "vulns/rust/idor-unscoped-record.rs", + "title": "IDOR through an unscoped record lookup", + "category": "rust", + "language": "rust", + "cwe": "CWE-639", + "cwes": [ + "CWE-639" + ], + "severity": "high", + "expected_detection": true, + "description": "A record is fetched by request ID without an ownership constraint.", + "detection_target": "Object lookup by request ID without subject scoping.", + "safe_guard": "Every example is behind `NEVER_RUN`; no store is queried.", + "attribution": "line", + "vulnerable_lines": [ + 23 + ], + "safe_lines": [ + 30 + ] + }, + { + "id": "rust-insecure-random-token", + "file": "vulns/rust/insecure-random-token.rs", + "title": "Predictable recovery token derived from system time", + "category": "rust", + "language": "rust", + "cwe": "CWE-330", + "cwes": [ + "CWE-330" + ], + "severity": "high", + "expected_detection": true, + "description": "A recovery token is derived from a predictable timestamp.", + "detection_target": "Time or non-cryptographic state used as a security token.", + "safe_guard": "Every example is behind `NEVER_RUN`; no token is generated.", + "attribution": "line", + "vulnerable_lines": [ + 28 + ], + "safe_lines": [ + 36 + ] + }, + { + "id": "rust-jwt-decode-without-verify", + "file": "vulns/rust/jwt-decode-without-verify.rs", + "title": "JWT claims trusted without signature verification", + "category": "rust", + "language": "rust", + "cwe": "CWE-347", + "cwes": [ + "CWE-347" + ], + "severity": "critical", + "expected_detection": true, + "description": "Claims are decoded and used before the token signature is verified.", + "detection_target": "Unverified JWT decoding used for authorization decisions.", + "safe_guard": "Every example is behind `NEVER_RUN`; no token is decoded.", + "attribution": "line", + "vulnerable_lines": [ + 23 + ], + "safe_lines": [ + 30 + ] + }, + { + "id": "rust-ldap-filter-injection", + "file": "vulns/rust/ldap-filter-injection.rs", + "title": "LDAP filter injection through string interpolation", + "category": "rust", + "language": "rust", + "cwe": "CWE-90", + "cwes": [ + "CWE-90" + ], + "severity": "high", + "expected_detection": true, + "description": "Request text is interpolated into an LDAP search filter.", + "detection_target": "Tainted data embedded in LDAP filter syntax.", + "safe_guard": "Every example is behind `NEVER_RUN`; no directory is queried.", + "attribution": "line", + "vulnerable_lines": [ + 18 + ], + "safe_lines": [ + 30 + ] + }, + { + "id": "rust-log-injection", + "file": "vulns/rust/log-injection.rs", + "title": "Log injection through unsanitized request data", + "category": "rust", + "language": "rust", + "cwe": "CWE-117", + "cwes": [ + "CWE-117" + ], + "severity": "medium", + "expected_detection": true, + "description": "Newlines from request data are embedded in a line-oriented log.", + "detection_target": "Tainted text interpolated into log messages.", + "safe_guard": "Every example is behind `NEVER_RUN`; nothing is logged.", + "attribution": "line", + "vulnerable_lines": [ + 18 + ], + "safe_lines": [ + 26 + ] + }, + { + "id": "rust-mass-assignment", + "file": "vulns/rust/mass-assignment.rs", + "title": "Mass assignment of untrusted account attributes", + "category": "rust", + "language": "rust", + "cwe": "CWE-915", + "cwes": [ + "CWE-915" + ], + "severity": "high", + "expected_detection": true, + "description": "Privileged fields are copied from a request model without filtering.", + "detection_target": "Whole-object assignment from an untrusted input model.", + "safe_guard": "Every example is behind `NEVER_RUN`; no state is changed.", + "attribution": "line", + "vulnerable_lines": [ + 32 + ], + "safe_lines": [ + 38 + ] + }, + { + "id": "rust-open-redirect", + "file": "vulns/rust/open-redirect.rs", + "title": "Open redirect via an unvalidated return URL", + "category": "rust", + "language": "rust", + "cwe": "CWE-601", + "cwes": [ + "CWE-601" + ], + "severity": "medium", + "expected_detection": true, + "description": "A request value is returned directly as a redirect destination.", + "detection_target": "Tainted redirect destinations without local-path validation.", + "safe_guard": "Every example is behind `NEVER_RUN`; no response is emitted.", + "attribution": "line", + "vulnerable_lines": [ + 18 + ], + "safe_lines": [ + 30 + ] + }, + { + "id": "rust-permissive-cors", + "file": "vulns/rust/permissive-cors.rs", + "title": "Credentialed CORS response reflects an arbitrary origin", + "category": "rust", + "language": "rust", + "cwe": "CWE-942", + "cwes": [ + "CWE-942" + ], + "severity": "high", + "expected_detection": true, + "description": "A response trusts Origin while also allowing credentials.", + "detection_target": "Reflected origins combined with credentialed CORS.", + "safe_guard": "Every example is behind `NEVER_RUN`; no headers are emitted.", + "attribution": "line", + "vulnerable_lines": [ + 19, + 20 + ], + "safe_lines": [ + 29 + ] + }, + { + "id": "rust-regex-dos-user-pattern", + "file": "vulns/rust/regex-dos-user-pattern.rs", + "title": "ReDoS through attacker-controlled pattern compilation", + "category": "rust", + "language": "rust", + "cwe": "CWE-1333", + "cwes": [ + "CWE-1333" + ], + "severity": "medium", + "expected_detection": true, + "description": "A request value is compiled and applied as a pattern.", + "detection_target": "Tainted pattern strings passed to a regex engine.", + "safe_guard": "Every example is behind `NEVER_RUN`; no pattern is compiled.", + "attribution": "line", + "vulnerable_lines": [ + 23 + ], + "safe_lines": [ + 30 + ] + }, + { + "id": "rust-unsafe-polymorphic-deserialization", + "file": "vulns/rust/unsafe-polymorphic-deserialization.rs", + "title": "Unsafe polymorphic deserialization of untrusted bytes", + "category": "rust", + "language": "rust", + "cwe": "CWE-502", + "cwes": [ + "CWE-502" + ], + "severity": "critical", + "expected_detection": true, + "description": "Untrusted bytes select and instantiate an arbitrary object type.", + "detection_target": "Polymorphic deserialization of attacker-controlled input.", + "safe_guard": "Every example is behind `NEVER_RUN`; no bytes are decoded.", + "attribution": "line", + "vulnerable_lines": [ + 23 + ], + "safe_lines": [ + 30 + ] + }, + { + "id": "rust-weak-password-hash", + "file": "vulns/rust/weak-password-hash.rs", + "title": "Password storage with a non-cryptographic hash", + "category": "rust", + "language": "rust", + "cwe": "CWE-916", + "cwes": [ + "CWE-916" + ], + "severity": "high", + "expected_detection": true, + "description": "DefaultHasher is fast, unsalted, and unsuitable for passwords.", + "detection_target": "Non-password hashes used to derive stored password verifiers.", + "safe_guard": "Every example is behind `NEVER_RUN`; no password is processed.", + "attribution": "line", + "vulnerable_lines": [ + 27 + ], + "safe_lines": [ + 34 + ] + }, + { + "id": "rust-xxe-parser", + "file": "vulns/rust/xxe-parser.rs", + "title": "XML parser resolves attacker-controlled external entities", + "category": "rust", + "language": "rust", + "cwe": "CWE-611", + "cwes": [ + "CWE-611" + ], + "severity": "high", + "expected_detection": true, + "description": "Untrusted XML is parsed with external entity resolution enabled.", + "detection_target": "Entity-resolving parser configuration for untrusted XML.", + "safe_guard": "Every example is behind `NEVER_RUN`; no XML is parsed.", + "attribution": "line", + "vulnerable_lines": [ + 23 + ], + "safe_lines": [ + 30 + ] + }, { "id": "secret-aws-credentials-hardcoded", "file": "vulns/secrets/aws-credentials-hardcoded.env", diff --git a/vulns/rust/csrf-missing-token.rs b/vulns/rust/csrf-missing-token.rs new file mode 100644 index 0000000..fcb5b27 --- /dev/null +++ b/vulns/rust/csrf-missing-token.rs @@ -0,0 +1,28 @@ +// @id rust-csrf-missing-token +// @test-case State-changing request accepted without CSRF verification +// @cwe CWE-352 +// @severity high +// @language rust +// @expected-detection true +// @description A cookie-authenticated mutation runs without a CSRF token check. +// @safe-guard Every example is behind `NEVER_RUN`; no mutation occurs. +// @detection-target State changes without synchronizer-token validation. +// +// NEVER RUN IN PRODUCTION — intentional test case for scanner validation. + +#![allow(dead_code)] +const NEVER_RUN: bool = false; + +fn apply_email_change(_email: &str) {} + +fn change_email_vulnerable(email: &str) { + if NEVER_RUN { + apply_email_change(email); // VULNERABLE: CWE-352 + } +} + +fn change_email_safe(email: &str, supplied_token: &str, expected_token: &str) { + if NEVER_RUN && supplied_token == expected_token { + apply_email_change(email); // SAFE: synchronizer token verified + } +} diff --git a/vulns/rust/hardcoded-crypto-key.rs b/vulns/rust/hardcoded-crypto-key.rs new file mode 100644 index 0000000..31fb75b --- /dev/null +++ b/vulns/rust/hardcoded-crypto-key.rs @@ -0,0 +1,28 @@ +// @id rust-hardcoded-crypto-key +// @test-case Hard-coded encryption key embedded in source +// @cwe CWE-321 +// @severity high +// @language rust +// @expected-detection true +// @description Encryption key material is stored as a source-code literal. +// @safe-guard Every example is behind `NEVER_RUN`; the literal is synthetic. +// @detection-target Static key literals used as cryptographic secrets. +// +// NEVER RUN IN PRODUCTION — intentional test case for scanner validation. + +#![allow(dead_code)] +const NEVER_RUN: bool = false; + +fn encryption_key_vulnerable() -> Option<&'static [u8]> { + if NEVER_RUN { + return Some(b"TEST_ONLY_AES_KEY_00000000000000"); // VULNERABLE: CWE-321 synthetic fixture + } + None +} + +fn encryption_key_safe() -> Option { + if NEVER_RUN { + return std::env::var("APP_ENCRYPTION_KEY").ok(); // SAFE: supplied by secret management + } + None +} diff --git a/vulns/rust/header-injection.rs b/vulns/rust/header-injection.rs new file mode 100644 index 0000000..da6df39 --- /dev/null +++ b/vulns/rust/header-injection.rs @@ -0,0 +1,29 @@ +// @id rust-header-injection +// @test-case HTTP response splitting through a tainted header value +// @cwe CWE-113 +// @severity high +// @language rust +// @expected-detection true +// @description Request text is concatenated into a response header. +// @safe-guard Every example is behind `NEVER_RUN`; no header is emitted. +// @detection-target CRLF-capable input used in response headers. +// +// NEVER RUN IN PRODUCTION — intentional test case for scanner validation. + +#![allow(dead_code)] +const NEVER_RUN: bool = false; + +fn attachment_vulnerable(name: &str) -> Option { + if NEVER_RUN { + return Some(format!("attachment; filename={name}")); // VULNERABLE: CWE-113 + } + None +} + +fn attachment_safe(name: &str) -> Option { + if NEVER_RUN { + let clean = name.replace(['\r', '\n', '"'], ""); + return Some(format!("attachment; filename=\"{clean}\"")); // SAFE: CRLF and delimiter removed + } + None +} diff --git a/vulns/rust/idor-unscoped-record.rs b/vulns/rust/idor-unscoped-record.rs new file mode 100644 index 0000000..b4839d9 --- /dev/null +++ b/vulns/rust/idor-unscoped-record.rs @@ -0,0 +1,33 @@ +// @id rust-idor-unscoped-record +// @test-case IDOR through an unscoped record lookup +// @cwe CWE-639 +// @severity high +// @language rust +// @expected-detection true +// @description A record is fetched by request ID without an ownership constraint. +// @safe-guard Every example is behind `NEVER_RUN`; no store is queried. +// @detection-target Object lookup by request ID without subject scoping. +// +// NEVER RUN IN PRODUCTION — intentional test case for scanner validation. + +#![allow(dead_code)] +const NEVER_RUN: bool = false; + +trait InvoiceStore { + fn find_by_id(&self, id: &str) -> Option; + fn find_by_owner_and_id(&self, owner: &str, id: &str) -> Option; +} + +fn invoice_vulnerable(store: &dyn InvoiceStore, requested_id: &str) -> Option { + if NEVER_RUN { + return store.find_by_id(requested_id); // VULNERABLE: CWE-639 + } + None +} + +fn invoice_safe(store: &dyn InvoiceStore, owner_id: &str, requested_id: &str) -> Option { + if NEVER_RUN { + return store.find_by_owner_and_id(owner_id, requested_id); // SAFE: lookup scoped to owner + } + None +} diff --git a/vulns/rust/insecure-random-token.rs b/vulns/rust/insecure-random-token.rs new file mode 100644 index 0000000..09efc64 --- /dev/null +++ b/vulns/rust/insecure-random-token.rs @@ -0,0 +1,40 @@ +// @id rust-insecure-random-token +// @test-case Predictable recovery token derived from system time +// @cwe CWE-330 +// @severity high +// @language rust +// @expected-detection true +// @description A recovery token is derived from a predictable timestamp. +// @safe-guard Every example is behind `NEVER_RUN`; no token is generated. +// @detection-target Time or non-cryptographic state used as a security token. +// +// NEVER RUN IN PRODUCTION — intentional test case for scanner validation. + +#![allow(dead_code)] +use std::time::{SystemTime, UNIX_EPOCH}; + +const NEVER_RUN: bool = false; + +trait SecureTokenSource { + fn fill(&self, output: &mut [u8]); +} + +fn reset_token_vulnerable() -> Option { + if NEVER_RUN { + let stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .ok()? + .as_millis(); + return Some(stamp.to_string()); // VULNERABLE: CWE-330 + } + None +} + +fn reset_token_safe(source: &dyn SecureTokenSource) -> Option<[u8; 32]> { + if NEVER_RUN { + let mut token = [0_u8; 32]; + source.fill(&mut token); // SAFE: cryptographically secure token source + return Some(token); + } + None +} diff --git a/vulns/rust/jwt-decode-without-verify.rs b/vulns/rust/jwt-decode-without-verify.rs new file mode 100644 index 0000000..7d1754f --- /dev/null +++ b/vulns/rust/jwt-decode-without-verify.rs @@ -0,0 +1,33 @@ +// @id rust-jwt-decode-without-verify +// @test-case JWT claims trusted without signature verification +// @cwe CWE-347 +// @severity critical +// @language rust +// @expected-detection true +// @description Claims are decoded and used before the token signature is verified. +// @safe-guard Every example is behind `NEVER_RUN`; no token is decoded. +// @detection-target Unverified JWT decoding used for authorization decisions. +// +// NEVER RUN IN PRODUCTION — intentional test case for scanner validation. + +#![allow(dead_code)] +const NEVER_RUN: bool = false; + +trait JwtVerifier { + fn decode_unverified(&self, token: &str) -> Option; + fn verify_rs256(&self, token: &str, public_key: &[u8]) -> Option; +} + +fn claims_vulnerable(verifier: &dyn JwtVerifier, token: &str) -> Option { + if NEVER_RUN { + return verifier.decode_unverified(token); // VULNERABLE: CWE-347 + } + None +} + +fn claims_safe(verifier: &dyn JwtVerifier, token: &str, public_key: &[u8]) -> Option { + if NEVER_RUN { + return verifier.verify_rs256(token, public_key); // SAFE: signature and algorithm verified + } + None +} diff --git a/vulns/rust/ldap-filter-injection.rs b/vulns/rust/ldap-filter-injection.rs new file mode 100644 index 0000000..9ce9228 --- /dev/null +++ b/vulns/rust/ldap-filter-injection.rs @@ -0,0 +1,33 @@ +// @id rust-ldap-filter-injection +// @test-case LDAP filter injection through string interpolation +// @cwe CWE-90 +// @severity high +// @language rust +// @expected-detection true +// @description Request text is interpolated into an LDAP search filter. +// @safe-guard Every example is behind `NEVER_RUN`; no directory is queried. +// @detection-target Tainted data embedded in LDAP filter syntax. +// +// NEVER RUN IN PRODUCTION — intentional test case for scanner validation. + +#![allow(dead_code)] +const NEVER_RUN: bool = false; + +fn ldap_filter_vulnerable(uid: &str) -> Option { + if NEVER_RUN { + return Some(format!("(&(objectClass=person)(uid={uid}))")); // VULNERABLE: CWE-90 + } + None +} + +fn ldap_filter_safe(uid: &str) -> Option { + if NEVER_RUN { + let escaped = uid + .replace('\\', r"\5c") + .replace('*', r"\2a") + .replace('(', r"\28") + .replace(')', r"\29"); + return Some(format!("(&(objectClass=person)(uid={escaped}))")); // SAFE: LDAP metacharacters escaped + } + None +} diff --git a/vulns/rust/log-injection.rs b/vulns/rust/log-injection.rs new file mode 100644 index 0000000..61a573e --- /dev/null +++ b/vulns/rust/log-injection.rs @@ -0,0 +1,29 @@ +// @id rust-log-injection +// @test-case Log injection through unsanitized request data +// @cwe CWE-117 +// @severity medium +// @language rust +// @expected-detection true +// @description Newlines from request data are embedded in a line-oriented log. +// @safe-guard Every example is behind `NEVER_RUN`; nothing is logged. +// @detection-target Tainted text interpolated into log messages. +// +// NEVER RUN IN PRODUCTION — intentional test case for scanner validation. + +#![allow(dead_code)] +const NEVER_RUN: bool = false; + +fn audit_vulnerable(username: &str) -> Option { + if NEVER_RUN { + return Some(format!("login user={username}")); // VULNERABLE: CWE-117 + } + None +} + +fn audit_safe(username: &str) -> Option { + if NEVER_RUN { + let clean = username.replace(['\r', '\n'], ""); + return Some(format!("login user={clean:?}")); // SAFE: line breaks removed and value escaped + } + None +} diff --git a/vulns/rust/mass-assignment.rs b/vulns/rust/mass-assignment.rs new file mode 100644 index 0000000..10b3ab6 --- /dev/null +++ b/vulns/rust/mass-assignment.rs @@ -0,0 +1,40 @@ +// @id rust-mass-assignment +// @test-case Mass assignment of untrusted account attributes +// @cwe CWE-915 +// @severity high +// @language rust +// @expected-detection true +// @description Privileged fields are copied from a request model without filtering. +// @safe-guard Every example is behind `NEVER_RUN`; no state is changed. +// @detection-target Whole-object assignment from an untrusted input model. +// +// NEVER RUN IN PRODUCTION — intentional test case for scanner validation. + +#![allow(dead_code)] +const NEVER_RUN: bool = false; + +#[derive(Clone)] +struct AccountInput { + email: String, + is_admin: bool, +} + +struct Account { + email: String, + is_admin: bool, +} + +fn update_vulnerable(account: &mut Account, input: AccountInput) { + if NEVER_RUN { + *account = Account { + email: input.email, + is_admin: input.is_admin, + }; // VULNERABLE: CWE-915 + } +} + +fn update_safe(account: &mut Account, input: AccountInput) { + if NEVER_RUN { + account.email = input.email; // SAFE: explicit non-privileged field allow-list + } +} diff --git a/vulns/rust/open-redirect.rs b/vulns/rust/open-redirect.rs new file mode 100644 index 0000000..61fd31e --- /dev/null +++ b/vulns/rust/open-redirect.rs @@ -0,0 +1,33 @@ +// @id rust-open-redirect +// @test-case Open redirect via an unvalidated return URL +// @cwe CWE-601 +// @severity medium +// @language rust +// @expected-detection true +// @description A request value is returned directly as a redirect destination. +// @safe-guard Every example is behind `NEVER_RUN`; no response is emitted. +// @detection-target Tainted redirect destinations without local-path validation. +// +// NEVER RUN IN PRODUCTION — intentional test case for scanner validation. + +#![allow(dead_code)] +const NEVER_RUN: bool = false; + +fn redirect_vulnerable(return_to: &str) -> Option { + if NEVER_RUN { + return Some(return_to.to_owned()); // VULNERABLE: CWE-601 + } + None +} + +fn redirect_safe(return_to: &str) -> Option { + if NEVER_RUN { + let target = if return_to.starts_with('/') && !return_to.starts_with("//") { + return_to + } else { + "/dashboard" + }; + return Some(target.to_owned()); // SAFE: constrained to a local absolute path + } + None +} diff --git a/vulns/rust/permissive-cors.rs b/vulns/rust/permissive-cors.rs new file mode 100644 index 0000000..07fcbd1 --- /dev/null +++ b/vulns/rust/permissive-cors.rs @@ -0,0 +1,32 @@ +// @id rust-permissive-cors +// @test-case Credentialed CORS response reflects an arbitrary origin +// @cwe CWE-942 +// @severity high +// @language rust +// @expected-detection true +// @description A response trusts Origin while also allowing credentials. +// @safe-guard Every example is behind `NEVER_RUN`; no headers are emitted. +// @detection-target Reflected origins combined with credentialed CORS. +// +// NEVER RUN IN PRODUCTION — intentional test case for scanner validation. + +#![allow(dead_code)] +const NEVER_RUN: bool = false; + +fn cors_vulnerable(origin: &str) -> Option> { + if NEVER_RUN { + return Some(vec![ + ("Access-Control-Allow-Origin".into(), origin.into()), // VULNERABLE: CWE-942 + ("Access-Control-Allow-Credentials".into(), "true".into()), // VULNERABLE: credentialed reflection + ]); + } + None +} + +fn cors_safe(origin: &str) -> Option> { + if NEVER_RUN && origin == "https://portal.example.invalid" { + return Some(vec![("Access-Control-Allow-Origin".into(), origin.into())]); + // SAFE: fixed allow-list + } + None +} diff --git a/vulns/rust/regex-dos-user-pattern.rs b/vulns/rust/regex-dos-user-pattern.rs new file mode 100644 index 0000000..79af816 --- /dev/null +++ b/vulns/rust/regex-dos-user-pattern.rs @@ -0,0 +1,33 @@ +// @id rust-regex-dos-user-pattern +// @test-case ReDoS through attacker-controlled pattern compilation +// @cwe CWE-1333 +// @severity medium +// @language rust +// @expected-detection true +// @description A request value is compiled and applied as a pattern. +// @safe-guard Every example is behind `NEVER_RUN`; no pattern is compiled. +// @detection-target Tainted pattern strings passed to a regex engine. +// +// NEVER RUN IN PRODUCTION — intentional test case for scanner validation. + +#![allow(dead_code)] +const NEVER_RUN: bool = false; + +trait PatternEngine { + fn compile_and_match(&self, pattern: &str, value: &str) -> bool; + fn literal_match(&self, literal: &str, value: &str) -> bool; +} + +fn match_vulnerable(engine: &dyn PatternEngine, pattern: &str, value: &str) -> bool { + if NEVER_RUN { + return engine.compile_and_match(pattern, value); // VULNERABLE: CWE-1333 + } + false +} + +fn match_safe(engine: &dyn PatternEngine, literal: &str, value: &str) -> bool { + if NEVER_RUN { + return engine.literal_match(literal, value); // SAFE: user value treated as a literal + } + false +} diff --git a/vulns/rust/unsafe-polymorphic-deserialization.rs b/vulns/rust/unsafe-polymorphic-deserialization.rs new file mode 100644 index 0000000..c8048f3 --- /dev/null +++ b/vulns/rust/unsafe-polymorphic-deserialization.rs @@ -0,0 +1,33 @@ +// @id rust-unsafe-polymorphic-deserialization +// @test-case Unsafe polymorphic deserialization of untrusted bytes +// @cwe CWE-502 +// @severity critical +// @language rust +// @expected-detection true +// @description Untrusted bytes select and instantiate an arbitrary object type. +// @safe-guard Every example is behind `NEVER_RUN`; no bytes are decoded. +// @detection-target Polymorphic deserialization of attacker-controlled input. +// +// NEVER RUN IN PRODUCTION — intentional test case for scanner validation. + +#![allow(dead_code)] +const NEVER_RUN: bool = false; + +trait Decoder { + fn decode_polymorphic(&self, input: &[u8]) -> Option; + fn decode_bounded_profile(&self, input: &[u8]) -> Option; +} + +fn decode_vulnerable(decoder: &dyn Decoder, input: &[u8]) -> Option { + if NEVER_RUN { + return decoder.decode_polymorphic(input); // VULNERABLE: CWE-502 + } + None +} + +fn decode_safe(decoder: &dyn Decoder, input: &[u8]) -> Option { + if NEVER_RUN && input.len() <= 4096 { + return decoder.decode_bounded_profile(input); // SAFE: bounded concrete schema + } + None +} diff --git a/vulns/rust/weak-password-hash.rs b/vulns/rust/weak-password-hash.rs new file mode 100644 index 0000000..c866848 --- /dev/null +++ b/vulns/rust/weak-password-hash.rs @@ -0,0 +1,37 @@ +// @id rust-weak-password-hash +// @test-case Password storage with a non-cryptographic hash +// @cwe CWE-916 +// @severity high +// @language rust +// @expected-detection true +// @description DefaultHasher is fast, unsalted, and unsuitable for passwords. +// @safe-guard Every example is behind `NEVER_RUN`; no password is processed. +// @detection-target Non-password hashes used to derive stored password verifiers. +// +// NEVER RUN IN PRODUCTION — intentional test case for scanner validation. + +#![allow(dead_code)] +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; + +const NEVER_RUN: bool = false; + +trait PasswordKdf { + fn derive(&self, password: &[u8], salt: &[u8]) -> Vec; +} + +fn password_hash_vulnerable(password: &str) -> Option { + if NEVER_RUN { + let mut hasher = DefaultHasher::new(); + password.hash(&mut hasher); + return Some(hasher.finish()); // VULNERABLE: CWE-916 + } + None +} + +fn password_hash_safe(kdf: &dyn PasswordKdf, password: &[u8], salt: &[u8]) -> Option> { + if NEVER_RUN { + return Some(kdf.derive(password, salt)); // SAFE: adaptive salted password KDF + } + None +} diff --git a/vulns/rust/xxe-parser.rs b/vulns/rust/xxe-parser.rs new file mode 100644 index 0000000..0fa6b5d --- /dev/null +++ b/vulns/rust/xxe-parser.rs @@ -0,0 +1,33 @@ +// @id rust-xxe-parser +// @test-case XML parser resolves attacker-controlled external entities +// @cwe CWE-611 +// @severity high +// @language rust +// @expected-detection true +// @description Untrusted XML is parsed with external entity resolution enabled. +// @safe-guard Every example is behind `NEVER_RUN`; no XML is parsed. +// @detection-target Entity-resolving parser configuration for untrusted XML. +// +// NEVER RUN IN PRODUCTION — intentional test case for scanner validation. + +#![allow(dead_code)] +const NEVER_RUN: bool = false; + +trait XmlParser { + fn parse_with_external_entities(&self, input: &str) -> Option; + fn parse_without_external_entities(&self, input: &str) -> Option; +} + +fn xml_vulnerable(parser: &dyn XmlParser, input: &str) -> Option { + if NEVER_RUN { + return parser.parse_with_external_entities(input); // VULNERABLE: CWE-611 + } + None +} + +fn xml_safe(parser: &dyn XmlParser, input: &str) -> Option { + if NEVER_RUN { + return parser.parse_without_external_entities(input); // SAFE: external entities disabled + } + None +}