diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index e273714..3f568da 100644 --- a/docs/VULNERABILITY_CATALOG.md +++ b/docs/VULNERABILITY_CATALOG.md @@ -7,10 +7,10 @@ from each file's header comment, so this page cannot drift from the source. ## Totals -- **Test cases:** 104 -- **Expected detections:** 104 -- **`VULNERABLE:` markers:** 187 (individual lines a scanner should flag) -- **`SAFE:` markers:** 115 (lines a scanner must not flag — the false-positive control group) +- **Test cases:** 105 +- **Expected detections:** 105 +- **`VULNERABLE:` markers:** 189 (individual lines a scanner should flag) +- **`SAFE:` markers:** 116 (lines a scanner must not flag — the false-positive control group) - **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text - **CWE categories:** 78 — 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-287, CWE-288, CWE-291, CWE-295, CWE-306, CWE-307, CWE-319, CWE-321, 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-441, CWE-460, CWE-472, CWE-475, CWE-480, CWE-488, CWE-489, CWE-502, CWE-506, CWE-509, CWE-512, CWE-521, CWE-525, CWE-532, CWE-598, CWE-601, CWE-602, CWE-611, CWE-613, CWE-614, CWE-639, CWE-643, CWE-681, CWE-693, CWE-759, CWE-776, CWE-798, 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 @@ -30,6 +30,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Unintended proxy via http.Transport Proxy function using user-controlled URL | [`cwe-441-go.go`](../vulns/go/cwe-441-go.go) | CWE-441 | high | yes | 1 vuln / 1 safe | | Unsafe use of reflection to invoke methods with attacker-controlled names | [`cwe-475-go.go`](../vulns/go/cwe-475-go.go) | CWE-475 | high | yes | 1 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 | +| Path traversal via unsanitized filepath.Join with request input | [`path-traversal-filepath.go`](../vulns/go/path-traversal-filepath.go) | CWE-22 | high | yes | 2 vuln / 1 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 | diff --git a/vulns/VULNERABILITY_CATALOG.json b/vulns/VULNERABILITY_CATALOG.json index 7969c8f..1ba1548 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": 104, - "expected_detections": 104, - "vulnerable_markers": 187, - "safe_markers": 115, + "test_cases": 105, + "expected_detections": 105, + "vulnerable_markers": 189, + "safe_markers": 116, "languages": [ "dotenv", "go", @@ -204,6 +204,30 @@ 71 ] }, + { + "id": "go-path-traversal-filepath", + "file": "vulns/go/path-traversal-filepath.go", + "title": "Path traversal via unsanitized filepath.Join with request input", + "category": "go", + "language": "go", + "cwe": "CWE-22", + "cwes": [ + "CWE-22" + ], + "severity": "high", + "expected_detection": true, + "description": "A file-serving handler builds a path with filepath.Join from a", + "detection_target": "Taint flow from request input into filepath.Join,", + "safe_guard": "Guarded by the always-false `neverRun` constant plus an `ignore`", + "attribution": "line", + "vulnerable_lines": [ + 36, + 49 + ], + "safe_lines": [ + 62 + ] + }, { "id": "go-sqli-fmt-sprintf", "file": "vulns/go/sqli-fmt-sprintf.go", diff --git a/vulns/go/path-traversal-filepath.go b/vulns/go/path-traversal-filepath.go new file mode 100644 index 0000000..f51c1b7 --- /dev/null +++ b/vulns/go/path-traversal-filepath.go @@ -0,0 +1,73 @@ +// @id go-path-traversal-filepath +// @test-case Path traversal via unsanitized filepath.Join with request input +// @cwe CWE-22 +// @severity high +// @language go +// @expected-detection true +// @description A file-serving handler builds a path with filepath.Join from a +// request-supplied segment. ".." reaches outside the intended +// root, exposing arbitrary files. The same flaw appears through +// http.ServeFile and os.ReadFile with a tainted filename that +// only checks strings.HasPrefix on the joined output, which a +// segment like "../.." defeats. +// @safe-guard Guarded by the always-false `neverRun` constant plus an `ignore` +// build tag; no file is ever opened and no path is resolved. +// There are no hosts and no credentials. +// @detection-target Taint flow from request input into filepath.Join, +// os.ReadFile, or http.ServeFile without a root-dir +// containment check. +// +// NEVER RUN IN PRODUCTION — intentional test case for scanner validation. + +//go:build ignore + +package vulns + +import ( + "net/http" + "os" + "path/filepath" +) + +func serveFileVulnerable(w http.ResponseWriter, r *http.Request) { + if neverRun { + base := "/srv/www" + name := r.URL.Query().Get("file") // SOURCE: "../../etc/passwd" + // VULNERABLE: CWE-22 — no containment check on the resolved path + http.ServeFile(w, r, filepath.Join(base, name)) + } +} + +func readFileVulnerable(r *http.Request) ([]byte, error) { + if neverRun { + base := "/data/reports" + name := r.URL.Path // SOURCE: attacker-controlled URL path + // Broken guard: exact-prefix match is not a directory containment check. + if !stringsHasPrefix(filepath.Join(base, name), base) { + return nil, os.ErrNotExist + } + // VULNERABLE: CWE-22 — "../" still escapes base via normalization + return os.ReadFile(filepath.Join(base, name)) + } + return nil, nil +} + +// serveFileSafe is the safe counterpart — the scanner should NOT flag this. +// @expected-detection false +func serveFileSafe(w http.ResponseWriter, r *http.Request) { + if neverRun { + base := "/srv/www" + name := filepath.Clean(r.URL.Query().Get("file")) + joined := filepath.Join(base, name) + // SAFE: both the cleaned input and joined path must stay under base + if !stringsHasPrefix(joined, base) { + http.Error(w, "forbidden", http.StatusForbidden) + return + } + http.ServeFile(w, r, joined) + } +} + +func stringsHasPrefix(s, prefix string) bool { + return len(s) >= len(prefix) && s[:len(prefix)] == prefix +} \ No newline at end of file