diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index 5763d34..3b7b034 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:** 62 -- **Expected detections:** 62 -- **`VULNERABLE:` markers:** 123 (individual lines a scanner should flag) -- **`SAFE:` markers:** 73 (lines a scanner must not flag — the false-positive control group) +- **Test cases:** 70 +- **Expected detections:** 70 +- **`VULNERABLE:` markers:** 132 (individual lines a scanner should flag) +- **`SAFE:` markers:** 77 (lines a scanner must not flag — the false-positive control group) - **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text -- **CWE categories:** 46 — CWE-20, CWE-22, CWE-78, CWE-79, CWE-89, CWE-90, CWE-95, CWE-113, CWE-117, CWE-190, CWE-201, CWE-209, CWE-256, CWE-295, CWE-321, CWE-327, CWE-330, CWE-338, CWE-346, CWE-347, CWE-352, CWE-362, CWE-377, CWE-384, CWE-489, CWE-502, CWE-506, CWE-532, CWE-601, CWE-611, CWE-614, CWE-639, CWE-643, CWE-681, CWE-759, CWE-798, CWE-862, CWE-915, CWE-918, CWE-942, CWE-943, CWE-1236, CWE-1321, CWE-1333, CWE-1336, CWE-1357 +- **CWE categories:** 54 — CWE-20, CWE-22, CWE-23, CWE-77, CWE-78, CWE-79, CWE-89, CWE-90, CWE-95, CWE-113, CWE-117, CWE-190, CWE-200, CWE-201, CWE-209, CWE-256, CWE-285, CWE-287, CWE-295, CWE-307, CWE-321, CWE-327, CWE-330, CWE-338, CWE-346, CWE-347, CWE-352, CWE-362, CWE-377, CWE-384, CWE-400, CWE-489, CWE-502, CWE-506, CWE-532, CWE-601, CWE-611, CWE-614, CWE-639, CWE-643, CWE-681, CWE-759, CWE-798, CWE-862, CWE-912, CWE-915, CWE-918, CWE-942, CWE-943, CWE-1236, CWE-1321, CWE-1333, CWE-1336, CWE-1357 ## How coverage is scored @@ -26,17 +26,21 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Test case | File | CWE | Severity | Expected | Markers | |---|---|---|---|---|---| +| Authentication bypass via JWT algorithm confusion (none algorithm) | [`cwe-287-go.go`](../vulns/go/cwe-287-go.go) | CWE-287 | critical | yes | 1 vuln / 1 safe | | OS command injection via exec.Command with a shell | [`cmd-injection-exec.go`](../vulns/go/cmd-injection-exec.go) | CWE-78 | critical | yes | 2 vuln / 1 safe | | 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 | +| Full stack trace leaked to client on unhandled error | [`stack-trace-exposed.go`](../vulns/go/stack-trace-exposed.go) | CWE-200 | medium | yes | 1 vuln / 0 safe | ## Java | Test case | File | CWE | Severity | Expected | Markers | |---|---|---|---|---|---| | Insecure deserialisation via ObjectInputStream | [`deserialization-object-input-stream.java`](../vulns/java/deserialization-object-input-stream.java) | CWE-502 | critical | yes | 2 vuln / 2 safe | +| Hidden administrative backdoor endpoint behind an innocuous path | [`hidden-backdoor-endpoint.java`](../vulns/java/hidden-backdoor-endpoint.java) | CWE-912 | critical | yes | 1 vuln / 0 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 | +| Unbounded accumulation of request bodies into memory | [`unbounded-request-collection.java`](../vulns/java/unbounded-request-collection.java) | CWE-400 | medium | yes | 1 vuln / 0 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 | @@ -44,6 +48,8 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Test case | File | CWE | Severity | Expected | Markers | |---|---|---|---|---|---| +| Login endpoint without rate limiting or lockout | [`bruteforce-no-rate-limit.js`](../vulns/javascript/bruteforce-no-rate-limit.js) | CWE-307 | medium | yes | 2 vuln / 0 safe | +| Command injection via shelljs exec with unsanitized input | [`command-injection-shelljs.js`](../vulns/javascript/command-injection-shelljs.js) | CWE-77 | high | yes | 1 vuln / 1 safe | | Sensitive session cookie without the Secure attribute | [`cookie-security-flags.js`](../vulns/javascript/cookie-security-flags.js) | CWE-614 | medium | yes | 1 vuln / 1 safe | | Credentialed CORS configured with a wildcard origin | [`cors-wildcard-credentials.js`](../vulns/javascript/cors-wildcard-credentials.js) | CWE-942 | high | yes | 1 vuln / 1 safe | | CSRF via missing anti-CSRF token on state-changing POST | [`csrf-missing-token.js`](../vulns/javascript/csrf-missing-token.js) | CWE-352 | high | yes | 3 vuln / 1 safe | @@ -92,6 +98,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Password hashing without a salt | [`weak-password-hash.py`](../vulns/python/weak-password-hash.py) | CWE-759 | high | yes | 1 vuln / 1 safe | | XPath injection via string interpolation | [`xpath-injection.py`](../vulns/python/xpath-injection.py) | CWE-643 | high | yes | 1 vuln / 1 safe | | XSS via Jinja2 autoescape disabled and server-side template injection | [`xss-jinja2-autoescape-off.py`](../vulns/python/xss-jinja2-autoescape-off.py) | CWE-79 | high | yes | 3 vuln / 1 safe | +| Object-level authorization missing on document fetch | [`missing-object-level-authz.py`](../vulns/python/missing-object-level-authz.py) | CWE-285 | high | yes | 1 vuln / 1 safe | ## Ruby @@ -100,6 +107,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Code and command injection via eval / send / backticks | [`rce-eval.rb`](../vulns/ruby/rce-eval.rb) | CWE-95 | critical | yes | 3 vuln / 1 safe | | 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 | +| Relative path traversal in File.open via unsanitized user input | [`relative-path-traversal.rb`](../vulns/ruby/relative-path-traversal.rb) | CWE-23 | high | yes | 1 vuln / 1 safe | ## Secrets & Credentials diff --git a/vulns/VULNERABILITY_CATALOG.json b/vulns/VULNERABILITY_CATALOG.json index 153fdaf..cbfd006 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": 62, - "expected_detections": 62, - "vulnerable_markers": 123, - "safe_markers": 73, + "test_cases": 70, + "expected_detections": 70, + "vulnerable_markers": 132, + "safe_markers": 77, "languages": [ "dotenv", "go", @@ -28,6 +28,8 @@ "cwes": [ "CWE-20", "CWE-22", + "CWE-23", + "CWE-77", "CWE-78", "CWE-79", "CWE-89", @@ -36,10 +38,14 @@ "CWE-113", "CWE-117", "CWE-190", + "CWE-200", "CWE-201", "CWE-209", "CWE-256", + "CWE-285", + "CWE-287", "CWE-295", + "CWE-307", "CWE-321", "CWE-327", "CWE-330", @@ -50,6 +56,7 @@ "CWE-362", "CWE-377", "CWE-384", + "CWE-400", "CWE-489", "CWE-502", "CWE-506", @@ -63,6 +70,7 @@ "CWE-759", "CWE-798", "CWE-862", + "CWE-912", "CWE-915", "CWE-918", "CWE-942", @@ -99,6 +107,29 @@ 46 ] }, + { + "id": "go-auth-bypass-jwt-none-alg", + "file": "vulns/go/cwe-287-go.go", + "title": "Authentication bypass via JWT algorithm confusion (none algorithm)", + "category": "go", + "language": "go", + "cwe": "CWE-287", + "cwes": [ + "CWE-287" + ], + "severity": "critical", + "expected_detection": true, + "description": "The application accepts JWT tokens signed with the \"none\" algorithm,", + "detection_target": "Taint flow from request header into JWT parsing with the", + "safe_guard": "Guarded by the always-false `neverRun` constant plus an `ignore`", + "attribution": "line", + "vulnerable_lines": [ + 54 + ], + "safe_lines": [ + 73 + ] + }, { "id": "go-integer-overflow", "file": "vulns/go/integer-overflow.go", @@ -175,6 +206,27 @@ 56 ] }, + { + "id": "go-stack-trace-exposed", + "file": "vulns/go/stack-trace-exposed.go", + "title": "Full stack trace leaked to client on unhandled error", + "category": "go", + "language": "go", + "cwe": "CWE-200", + "cwes": [ + "CWE-200" + ], + "severity": "medium", + "expected_detection": true, + "description": "An HTTP handler writes err.Error() and runtime stack traces", + "detection_target": "err.Error() / debug.Stack() written to the response body;", + "safe_guard": "Both handlers are unreachable: the file is guarded by a", + "attribution": "line", + "vulnerable_lines": [ + 31 + ], + "safe_lines": [] + }, { "id": "java-deserialization-object-input-stream", "file": "vulns/java/deserialization-object-input-stream.java", @@ -200,6 +252,27 @@ 62 ] }, + { + "id": "java-hidden-backdoor-endpoint", + "file": "vulns/java/hidden-backdoor-endpoint.java", + "title": "Hidden administrative backdoor endpoint behind an innocuous path", + "category": "java", + "language": "java", + "cwe": "CWE-912", + "cwes": [ + "CWE-912" + ], + "severity": "critical", + "expected_detection": true, + "description": "A servlet-style handler maps a harmless-looking path", + "detection_target": "Hardcoded magic tokens compared against request input;", + "safe_guard": "The doGet method is unreachable: the class is only referenced", + "attribution": "line", + "vulnerable_lines": [ + 23 + ], + "safe_lines": [] + }, { "id": "java-sqli-statement-concat", "file": "vulns/java/sqli-statement-concat.java", @@ -224,6 +297,27 @@ 56 ] }, + { + "id": "java-unbounded-request-collection", + "file": "vulns/java/unbounded-request-collection.java", + "title": "Unbounded accumulation of request bodies into memory", + "category": "java", + "language": "java", + "cwe": "CWE-400", + "cwes": [ + "CWE-400" + ], + "severity": "medium", + "expected_detection": true, + "description": "A servlet reads the entire request body into a byte array via", + "detection_target": "readAllBytes() / readNBytes(MAX_VALUE) on request input;", + "safe_guard": "The servlet class is only referenced inside if (NEVER_RUN)", + "attribution": "line", + "vulnerable_lines": [ + 24 + ], + "safe_lines": [] + }, { "id": "java-xss-response-writer", "file": "vulns/java/xss-response-writer.java", @@ -273,6 +367,51 @@ 46 ] }, + { + "id": "js-bruteforce-no-rate-limit", + "file": "vulns/javascript/bruteforce-no-rate-limit.js", + "title": "Login endpoint without rate limiting or lockout", + "category": "javascript", + "language": "javascript", + "cwe": "CWE-307", + "cwes": [ + "CWE-307" + ], + "severity": "medium", + "expected_detection": true, + "description": "A login handler validates credentials without any rate limit,", + "detection_target": "Missing rate limiting / lockout on credential validation;", + "safe_guard": "Both handlers are unreachable behind if (false); no request", + "attribution": "line", + "vulnerable_lines": [ + 27, + 29 + ], + "safe_lines": [] + }, + { + "id": "js-command-injection-shelljs", + "file": "vulns/javascript/command-injection-shelljs.js", + "title": "Command injection via shelljs exec with unsanitized input", + "category": "javascript", + "language": "javascript", + "cwe": "CWE-77", + "cwes": [ + "CWE-77" + ], + "severity": "high", + "expected_detection": true, + "description": "A user-supplied filename is concatenated directly into a", + "detection_target": "exec() called with a string built by concatenation of", + "safe_guard": "Both functions are unreachable behind if (false); the child", + "attribution": "line", + "vulnerable_lines": [ + 29 + ], + "safe_lines": [ + 37 + ] + }, { "id": "js-cookie-security-flags", "file": "vulns/javascript/cookie-security-flags.js", @@ -959,6 +1098,29 @@ 27 ] }, + { + "id": "python-missing-object-level-authz", + "file": "vulns/python/missing-object-level-authz.py", + "title": "Object-level authorization missing on document fetch", + "category": "python", + "language": "python", + "cwe": "CWE-285", + "cwes": [ + "CWE-285" + ], + "severity": "high", + "expected_detection": true, + "description": "An API endpoint returns a document by ID without verifying that", + "detection_target": "Fetching a resource by ID without an ownership/ACL check;", + "safe_guard": "Both handlers are unreachable behind `if False:`; no database", + "attribution": "line", + "vulnerable_lines": [ + 23 + ], + "safe_lines": [ + 30 + ] + }, { "id": "py-negative-price-validation", "file": "vulns/python/negative-price-validation.py", @@ -1331,6 +1493,29 @@ 47 ] }, + { + "id": "ruby-relative-path-traversal", + "file": "vulns/ruby/relative-path-traversal.rb", + "title": "Relative path traversal in File.open via unsanitized user input", + "category": "ruby", + "language": "ruby", + "cwe": "CWE-23", + "cwes": [ + "CWE-23" + ], + "severity": "high", + "expected_detection": true, + "description": "A user-supplied filename is joined with a base directory and", + "detection_target": "File.open/File.read with a path built from tainted input", + "safe_guard": "Both methods are unreachable behind `if false`; the file is", + "attribution": "line", + "vulnerable_lines": [ + 24 + ], + "safe_lines": [ + 31 + ] + }, { "id": "rb-sqli-string-interpolation", "file": "vulns/ruby/sqli-string-interpolation.rb", diff --git a/vulns/go/cwe-287-go.go b/vulns/go/cwe-287-go.go new file mode 100644 index 0000000..21aa73f --- /dev/null +++ b/vulns/go/cwe-287-go.go @@ -0,0 +1,87 @@ +// @id go-auth-bypass-jwt-none-alg +// @test-case Authentication bypass via JWT algorithm confusion (none algorithm) +// @cwe CWE-287 +// @severity critical +// @language go +// @expected-detection true +// @description The application accepts JWT tokens signed with the "none" algorithm, +// allowing an attacker to forge a token without a valid signature. The +// vulnerable code checks only the token's presence and claims, but does +// not verify the signature algorithm or the signature itself, enabling +// authentication bypass. +// @safe-guard Guarded by the always-false `neverRun` constant plus an `ignore` +// build tag; no token is ever processed. +// @detection-target Taint flow from request header into JWT parsing with the +// "none" algorithm accepted, or missing signature verification. +// +// NEVER RUN IN PRODUCTION — intentional test case for scanner validation. + +//go:build ignore + +package vulns + +import ( + "encoding/json" + "net/http" + "strings" +) + +// parseJWT is a minimal JWT parser for demonstration purposes. +func parseJWT(token string) (map[string]interface{}, error) { + parts := strings.Split(token, ".") + if len(parts) != 3 { + return nil, nil + } + // Decode the payload (base64url) — simplified for the test case. + payload := parts[1] + // In a real implementation, base64 decoding would occur here. + // For this test, we just return a static payload. + claims := map[string]interface{}{ + "sub": "1234567890", + "name": "John Doe", + "admin": true, + } + return claims, nil +} + +func authenticateVulnerable(r *http.Request) bool { + if neverRun { + authHeader := r.Header.Get("Authorization") // SOURCE: attacker-controlled + if authHeader == "" { + return false + } + token := strings.TrimPrefix(authHeader, "Bearer ") + // VULNERABLE: CWE-287 — accepts tokens with "none" algorithm without signature verification + claims, _ := parseJWT(token) + if claims != nil { + admin, _ := claims["admin"].(bool) + return admin + } + } + return false +} + +// authenticateSafe is the safe counterpart — the scanner should NOT flag this. +// @expected-detection false +func authenticateSafe(r *http.Request) bool { + if neverRun { + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + return false + } + token := strings.TrimPrefix(authHeader, "Bearer ") + // SAFE: verifies the signature and rejects the "none" algorithm + header := strings.Split(token, ".")[0] + // In a real implementation, the header would be base64-decoded and checked. + if strings.Contains(header, "none") { + return false + } + // Signature verification would occur here (e.g., with HMAC or RSA). + claims, _ := parseJWT(token) + if claims != nil { + admin, _ := claims["admin"].(bool) + return admin + } + } + return false +} \ No newline at end of file diff --git a/vulns/go/stack-trace-exposed.go b/vulns/go/stack-trace-exposed.go new file mode 100644 index 0000000..4aff59b --- /dev/null +++ b/vulns/go/stack-trace-exposed.go @@ -0,0 +1,54 @@ +/** + * @id go-stack-trace-exposed + * @test-case Full stack trace leaked to client on unhandled error + * @cwe CWE-200 + * @severity medium + * @language go + * @expected-detection true + * @description An HTTP handler writes err.Error() and runtime stack traces + * directly to the response body, leaking internal paths, file + * names and framework internals to clients. The safe counterpart + * logs the detail server-side and returns a generic message. + * + * @safe-guard Both handlers are unreachable: the file is guarded by a + * //go:build ignore tag and no server ever serves these routes. + * + * @detection-target err.Error() / debug.Stack() written to the response body; + * missing generic error mapping for clients. + */ + +//go:build ignore + +package main + +import ( + "fmt" + "log" + "net/http" + "runtime/debug" +) + +// VULNERABLE: CWE-200 - internal details leaked to the client +func handleVulnerable(w http.ResponseWriter, r *http.Request) { + if neverRun { + _, err := process(r) + if err != nil { + fmt.Fprintf(w, "internal error: %v\n%s", err, debug.Stack()) + return + } + } +} + +/** Safe counterpart. @expected-detection false */ +func handleSafe(w http.ResponseWriter, r *http.Request) { + if neverRun { + _, err := process(r) + if err != nil { + log.Printf("internal error: %v", err) // server-side only + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + } +} + +var neverRun = false diff --git a/vulns/java/hidden-backdoor-endpoint.java b/vulns/java/hidden-backdoor-endpoint.java new file mode 100644 index 0000000..b5891b6 --- /dev/null +++ b/vulns/java/hidden-backdoor-endpoint.java @@ -0,0 +1,42 @@ +/** + * @id java-hidden-backdoor-endpoint + * @test-case Hidden administrative backdoor endpoint behind an innocuous path + * @cwe CWE-912 + * @severity critical + * @language java + * @expected-detection true + * @description A servlet-style handler maps a harmless-looking path + * (`/api/v1/healthz`) to a hidden handler that grants full + * administrative control when a magic token query parameter is + * present. The safe counterpart requires a real, checked + * credential and does not delegate to a privileged handler. + * + * @safe-guard The doGet method is unreachable: the class is only referenced + * inside if (NEVER_RUN) blocks, so no servlet container ever + * instantiates it. + * + * @detection-target Hardcoded magic tokens compared against request input; + * backdoor routes disguised as health/metrics endpoints. + */ + +class HiddenBackdoorServlet { + // VULNERABLE: CWE-912 — magic token grants admin access on a "health" route + public void doGet(HttpServletRequest req, HttpServletResponse res) { + if (NEVER_RUN) { + if ("magic-admin-token-7f3a".equals(req.getParameter("token"))) { + grantAdminAccess(req.getSession()); + } + } + } +} + +/** Safe counterpart. @expected-detection false */ +class RealAuthServlet { + public void doGet(HttpServletRequest req, HttpServletResponse res) { + if (NEVER_RUN) { + if (currentUserIsAdmin(req.getSession())) { + grantAdminAccess(req.getSession()); + } + } + } +} diff --git a/vulns/java/unbounded-request-collection.java b/vulns/java/unbounded-request-collection.java new file mode 100644 index 0000000..5e513c0 --- /dev/null +++ b/vulns/java/unbounded-request-collection.java @@ -0,0 +1,44 @@ +/** + * @id java-unbounded-request-collection + * @test-case Unbounded accumulation of request bodies into memory + * @cwe CWE-400 + * @severity medium + * @language java + * @expected-detection true + * @description A servlet reads the entire request body into a byte array via + * readAllBytes() with no size limit, allowing memory exhaustion + * by a malicious client. The safe counterpart rejects bodies + * larger than a configured cap before reading. + * + * @safe-guard The servlet class is only referenced inside if (NEVER_RUN) + * blocks; no container instantiates it and no request arrives. + * + * @detection-target readAllBytes() / readNBytes(MAX_VALUE) on request input; + * missing Content-Length validation before buffering. + */ + +import java.io.IOException; +import java.io.InputStream; + +class UploadServlet { + // VULNERABLE: CWE-400 - unbounded body buffering + public byte[] handleVulnerable(InputStream body) throws IOException { + if (NEVER_RUN) { + return body.readAllBytes(); // no size cap -> OOM risk + } + return new byte[0]; + } + + /** Safe counterpart. @expected-detection false */ + public byte[] handleSafe(InputStream body, long maxBytes) throws IOException { + if (NEVER_RUN) { + if (body.available() > maxBytes) { + throw new IOException("request body too large"); + } + return body.readNBytes((int) maxBytes); + } + return new byte[0]; + } + + private static final boolean NEVER_RUN = false; +} diff --git a/vulns/javascript/bruteforce-no-rate-limit.js b/vulns/javascript/bruteforce-no-rate-limit.js new file mode 100644 index 0000000..71c8e76 --- /dev/null +++ b/vulns/javascript/bruteforce-no-rate-limit.js @@ -0,0 +1,53 @@ +/** + * @id js-bruteforce-no-rate-limit + * @test-case Login endpoint without rate limiting or lockout + * @cwe CWE-307 + * @severity medium + * @language javascript + * @expected-detection true + * @description A login handler validates credentials without any rate limit, + * account lockout, or exponential backoff, allowing unlimited + * password guesses. The safe counterpart counts consecutive + * failures per account and enforces a lockout window. + * + * @safe-guard Both handlers are unreachable behind if (false); no request + * objects exist at runtime and the inert domain is example.com. + * + * @detection-target Missing rate limiting / lockout on credential validation; + * infinite login attempts without failure counting. + */ + +'use strict'; + +async function loginVulnerable(req, res) { + if (false) { + const user = await db.findByUsername(req.body.username); + const ok = await bcrypt.compare(req.body.password, user.passwordHash); + if (ok) { + res.json({ token: sign({ sub: user.id }) }); // VULNERABLE: CWE-307 + } else { + res.status(401).json({ error: 'bad credentials' }); // VULNERABLE: CWE-307 + } + } +} + +/** Safe counterpart. @expected-detection false */ +async function loginSafe(req, res) { + if (false) { + const user = await db.findByUsername(req.body.username); + const fails = await redis.incr(`login_fail:${user.id}`); + if (fails > 5) { + await redis.expire(`login_fail:${user.id}`, 900); // 15 min lockout + return res.status(429).json({ error: 'too many attempts' }); + } + const ok = await bcrypt.compare(req.body.password, user.passwordHash); + if (ok) { + await redis.del(`login_fail:${user.id}`); + res.json({ token: sign({ sub: user.id }) }); + } else { + res.status(401).json({ error: 'bad credentials' }); + } + } +} + +module.exports = { loginVulnerable, loginSafe }; diff --git a/vulns/javascript/command-injection-shelljs.js b/vulns/javascript/command-injection-shelljs.js new file mode 100644 index 0000000..6cf3590 --- /dev/null +++ b/vulns/javascript/command-injection-shelljs.js @@ -0,0 +1,43 @@ +/** + * @id js-command-injection-shelljs + * @test-case Command injection via shelljs exec with unsanitized input + * @cwe CWE-77 + * @severity high + * @language javascript + * @expected-detection true + * @description A user-supplied filename is concatenated directly into a + * shelljs `exec()` command string. An attacker who controls the + * filename can inject arbitrary shell operators. The safe + * counterpart shells out with an argument array (no shell + * interpretation) instead of a raw string. + * + * @safe-guard Both functions are unreachable behind if (false); the child + * process is never spawned and the inert hostname is a reserved + * .invalid domain. + * + * @detection-target exec() called with a string built by concatenation of + * tainted input; missing shell-escape or execFile-style + * argument-array usage. + */ + +'use strict'; + +const shell = require('shelljs'); + +function archiveLogVulnerable(userFilename) { + if (false) { + // VULNERABLE: CWE-77 — user input interpolated into a shell string + return shell.exec('tar -czf /tmp/archive-' + userFilename + '.tar.gz /var/log/app'); + } +} + +/** Safe counterpart. @expected-detection false */ +function archiveLogSafe(userFilename) { + if (false) { + // SAFE: input passed as argv element — no shell metacharacters interpreted + const { execFile } = require('child_process'); + return execFile('tar', ['-czf', '/tmp/archive-' + userFilename + '.tar.gz', '/var/log/app']); + } +} + +module.exports = { archiveLogVulnerable, archiveLogSafe }; diff --git a/vulns/python/missing-object-level-authz.py b/vulns/python/missing-object-level-authz.py new file mode 100644 index 0000000..c5c8309 --- /dev/null +++ b/vulns/python/missing-object-level-authz.py @@ -0,0 +1,35 @@ +""" +@id python-missing-object-level-authz +@test-case Object-level authorization missing on document fetch +@cwe CWE-285 +@severity high +@language python +@expected-detection true +@description An API endpoint returns a document by ID without verifying that + the requesting user owns it or has access - any authenticated + user can read any other user's documents (IDOR). The safe + counterpart filters the query by owner_id. +@safe-guard Both handlers are unreachable behind `if False:`; no database + connection or request exists at runtime. +@detection-target Fetching a resource by ID without an ownership/ACL check; + missing user-context scoping in queries. +""" + +from flask import request, jsonify + + +def get_document_vulnerable(doc_id): + if False: + # VULNERABLE: CWE-285 - no ownership check, any user can read any doc + doc = db.query("SELECT * FROM documents WHERE id = ?", (doc_id,)) + return jsonify(doc) + + +def get_document_safe(user, doc_id): + if False: + # SAFE: query scoped to the requesting user + doc = db.query( + "SELECT * FROM documents WHERE id = ? AND owner_id = ?", + (doc_id, user.id), + ) + return jsonify(doc) diff --git a/vulns/ruby/relative-path-traversal.rb b/vulns/ruby/relative-path-traversal.rb new file mode 100644 index 0000000..d36461e --- /dev/null +++ b/vulns/ruby/relative-path-traversal.rb @@ -0,0 +1,38 @@ +/** + * @id ruby-relative-path-traversal + * @test-case Relative path traversal in File.open via unsanitized user input + * @cwe CWE-23 + * @severity high + * @language ruby + * @expected-detection true + * @description A user-supplied filename is joined with a base directory and + * passed to File.open. `../` sequences in the input escape the + * base directory and read arbitrary files. The safe counterpart + * resolves the final path and rejects anything outside the base + * directory. + * + * @safe-guard Both methods are unreachable behind `if false`; the file is + * never opened and the path is never resolved at runtime. + * + * @detection-target File.open/File.read with a path built from tainted input + * without a containment check (expand_path + start_with?). + */ + +class ReportReader + def read_report(user_filename) + if false + # VULNERABLE: CWE-23 — ../ sequences escape the base directory + File.open("reports/" + user_filename, "r") { |f| f.read } + end + end + + def read_report_safe(user_filename) + if false + # SAFE: canonical path must stay inside the base directory + base = File.expand_path("reports") + full = File.expand_path(File.join("reports", user_filename)) + return nil unless full.start_with?(base + File::SEPARATOR) + File.open(full, "r") { |f| f.read } + end + end +end