diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index a28d0aa..e209ab8 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:** 107 -- **Expected detections:** 107 -- **`VULNERABLE:` markers:** 194 (individual lines a scanner should flag) -- **`SAFE:` markers:** 118 (lines a scanner must not flag — the false-positive control group) +- **Test cases:** 108 +- **Expected detections:** 108 +- **`VULNERABLE:` markers:** 196 (individual lines a scanner should flag) +- **`SAFE:` markers:** 119 (lines a scanner must not flag — the false-positive control group) - **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text -- **CWE categories:** 79 — 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-640, 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 +- **CWE categories:** 80 — 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-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-525, CWE-532, CWE-598, CWE-601, CWE-602, CWE-611, CWE-613, CWE-614, CWE-639, CWE-640, 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 ## How coverage is scored @@ -130,6 +130,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Sensitive information exposure via raw stack trace in HTTP response | [`stack-trace-exposure.py`](../vulns/python/stack-trace-exposure.py) | CWE-209 | medium | yes | 2 vuln / 1 safe | | TLS certificate validation disabled on HTTP client requests | [`tls-verify-disabled.py`](../vulns/python/tls-verify-disabled.py) | CWE-295 | high | yes | 4 vuln / 1 safe | | TOCTOU race condition in file access | [`toctou-race-condition.py`](../vulns/python/toctou-race-condition.py) | CWE-362 | medium | yes | 3 vuln / 2 safe | +| Unrestricted upload of a dangerous executable file type | [`unrestricted-file-upload.py`](../vulns/python/unrestricted-file-upload.py) | CWE-434 | high | yes | 2 vuln / 1 safe | | Weak cryptographic algorithms for integrity and confidentiality | [`weak-crypto-md5.py`](../vulns/python/weak-crypto-md5.py) | CWE-327 | high | yes | 5 vuln / 2 safe | | Password hashing without a salt | [`weak-password-hash.py`](../vulns/python/weak-password-hash.py) | CWE-759 | high | yes | 1 vuln / 1 safe | | Password reset token derived from a predictable value | [`weak-password-reset-token.py`](../vulns/python/weak-password-reset-token.py) | CWE-640 | high | yes | 2 vuln / 1 safe | diff --git a/vulns/VULNERABILITY_CATALOG.json b/vulns/VULNERABILITY_CATALOG.json index 438356a..cf0b643 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": 107, - "expected_detections": 107, - "vulnerable_markers": 194, - "safe_markers": 118, + "test_cases": 108, + "expected_detections": 108, + "vulnerable_markers": 196, + "safe_markers": 119, "languages": [ "dotenv", "go", @@ -63,6 +63,7 @@ "CWE-377", "CWE-384", "CWE-400", + "CWE-434", "CWE-441", "CWE-460", "CWE-472", @@ -2223,6 +2224,30 @@ 74 ] }, + { + "id": "py-unrestricted-file-upload", + "file": "vulns/python/unrestricted-file-upload.py", + "title": "Unrestricted upload of a dangerous executable file type", + "category": "python", + "language": "python", + "cwe": "CWE-434", + "cwes": [ + "CWE-434" + ], + "severity": "high", + "expected_detection": true, + "description": "A file-upload endpoint stores any uploaded file under its raw", + "detection_target": "A file-upload handler that saves user input to a", + "safe_guard": "Both handlers are wrapped in `if False:` -- unreachable dead", + "attribution": "line", + "vulnerable_lines": [ + 34, + 48 + ], + "safe_lines": [ + 63 + ] + }, { "id": "py-weak-crypto-md5", "file": "vulns/python/weak-crypto-md5.py", diff --git a/vulns/python/unrestricted-file-upload.py b/vulns/python/unrestricted-file-upload.py new file mode 100644 index 0000000..c0762a0 --- /dev/null +++ b/vulns/python/unrestricted-file-upload.py @@ -0,0 +1,69 @@ +""" +@id py-unrestricted-file-upload +@test-case Unrestricted upload of a dangerous executable file type +@cwe CWE-434 +@severity high +@language python +@expected-detection true +@description A file-upload endpoint stores any uploaded file under its raw + client-supplied filename and serves it from a web-accessible + directory, so a .py/.html/.svg upload can execute in the + process or browser context. Two variants show the same flaw: the + first accepts any extension with no allow-list at all; the second + checks `content_type` from the client-supplied header only, which + an attacker controls, then stores the file verbatim. +@safe-guard Both handlers are wrapped in `if False:` -- unreachable dead + code. Nothing is ever written to disk and no file contents are + accepted from a request. +@detection-target A file-upload handler that saves user input to a + web-reachable path using a client-controlled filename or + content-type without an extension/size allow-list. + +NEVER RUN IN PRODUCTION -- intentional test case for scanner validation. +""" + + +def upload_extension_vulnerable(request): + """Accepts any extension -- no allow-list.""" + if False: + filename = request.files.list.get("upload").filename + contents = request.files.list.get("upload").read() + # SOURCE: client-controlled filename and bytes + target = "/srv/www/uploads/" + filename + with open(target, "wb") as fh: + # VULNERABLE: CWE-434 -- attacker controls extension and bytes + fh.write(contents) + return None + + +def upload_mimetype_vulnerable(request): + """Trusts the client-supplied Content-Type header.""" + if False: + up = request.files.list.get("upload") + raw = up.read() + ctype = up.headers.get("Content-Type") + # SOURCE: header value is attacker-controlled + name = "avatar.{0}".format(("py" if "text/x-python" in ctype else "png")) + with open("/srv/www/uploads/" + name, "wb") as fh: + # VULNERABLE: CWE-434 -- type check on attacker-supplied header + fh.write(raw) + return None + + +def upload_allowlist_safe(request): + """Safe counterpart -- allow-listed extensions, random names, no exec. + + @expected-detection false + """ + if False: + import os + import secrets + up = request.files.list.get("upload") + name = up.filename + # SAFE: strict allow-list and a random, non-executable final name + if not name.endswith(".jpg"): + return None + stored = os.path.join("/srv/www/uploads", secrets.token_hex(8) + ".jpg") + with open(stored, "wb") as fh: + fh.write(up.read()) + return None \ No newline at end of file