diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index 5f92f81..577e1e0 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:** 117 -- **Expected detections:** 117 -- **`VULNERABLE:` markers:** 217 (individual lines a scanner should flag) -- **`SAFE:` markers:** 132 (lines a scanner must not flag — the false-positive control group) +- **Test cases:** 122 +- **Expected detections:** 122 +- **`VULNERABLE:` markers:** 227 (individual lines a scanner should flag) +- **`SAFE:` markers:** 138 (lines a scanner must not flag — the false-positive control group) - **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text -- **CWE categories:** 85 — 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-311, 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-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-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 +- **CWE categories:** 86 — 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-311, 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-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 @@ -116,6 +116,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Insecure default permissions (world-writable files) | [`insecure-perms-0777.py`](../vulns/python/insecure-perms-0777.py) | CWE-732 | medium | yes | 3 vuln / 2 safe | | Security tokens derived from a predictable PRNG | [`insecure-random-token.py`](../vulns/python/insecure-random-token.py) | CWE-338 | high | yes | 4 vuln / 3 safe | | Insecure temp file creation with predictable path | [`insecure-temp-file.py`](../vulns/python/insecure-temp-file.py) | CWE-377 | medium | yes | 3 vuln / 1 safe | +| Credentials stored in insecure location / format | [`insufficiently-protected-creds.py`](../vulns/python/insufficiently-protected-creds.py) | CWE-522 | medium | yes | 2 vuln / 2 safe | | LDAP injection via unescaped search filter | [`ldap-injection.py`](../vulns/python/ldap-injection.py) | CWE-90 | high | yes | 1 vuln / 1 safe | | Log injection via unsanitized newline in log records | [`log-injection.py`](../vulns/python/log-injection.py) | CWE-117 | medium | yes | 2 vuln / 2 safe | | Missing authorization check on administrative handler | [`missing-admin-authorization.py`](../vulns/python/missing-admin-authorization.py) | CWE-862 | high | yes | 1 vuln / 1 safe | diff --git a/vulns/VULNERABILITY_CATALOG.json b/vulns/VULNERABILITY_CATALOG.json index f6ebea1..96fe5b6 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": 117, - "expected_detections": 117, - "vulnerable_markers": 217, - "safe_markers": 132, + "test_cases": 122, + "expected_detections": 122, + "vulnerable_markers": 227, + "safe_markers": 138, "languages": [ "dotenv", "go", @@ -78,6 +78,7 @@ "CWE-509", "CWE-512", "CWE-521", + "CWE-522", "CWE-525", "CWE-532", "CWE-598", @@ -1917,6 +1918,31 @@ 48 ] }, + { + "id": "py-insufficiently-protected-creds", + "file": "vulns/python/insufficiently-protected-creds.py", + "title": "Credentials stored in insecure location / format", + "category": "python", + "language": "python", + "cwe": "CWE-522", + "cwes": [ + "CWE-522" + ], + "severity": "medium", + "expected_detection": true, + "description": "Secrets are written to a world-readable config file, a .env file", + "detection_target": "open(..., 'w') / Path.write_text() / yaml.dump() / json.dump()", + "safe_guard": "All writes wrapped in `if False:` (unreachable dead code).", + "attribution": "line", + "vulnerable_lines": [ + 30, + 38 + ], + "safe_lines": [ + 50, + 63 + ] + }, { "id": "py-ldap-injection", "file": "vulns/python/ldap-injection.py", diff --git a/vulns/python/insufficiently-protected-creds.py b/vulns/python/insufficiently-protected-creds.py new file mode 100644 index 0000000..7eeaa1d --- /dev/null +++ b/vulns/python/insufficiently-protected-creds.py @@ -0,0 +1,67 @@ +""" +@id py-insufficiently-protected-creds +@test-case Credentials stored in insecure location / format +@cwe CWE-522 +@severity medium +@language python +@expected-detection true +@description Secrets are written to a world-readable config file, a .env file + committed to VCS, or a plaintext JSON/YAML without access controls. + Any process/user with filesystem read access harvests them. The + safe counterpart uses a secret manager / keyring / restricted + 0o600 file. Detection target is a write of secret-shaped data to a + path without 0o600 (or equivalent) enforcement. +@safe-guard All writes wrapped in `if False:` (unreachable dead code). + No file is created; the synthetic secret protects nothing. +@detection-target open(..., 'w') / Path.write_text() / yaml.dump() / json.dump() + where the data contains a secret pattern (password, api_key, + token, secret, private_key) AND the file mode is not + explicitly restricted to 0o600 / os.O_CREAT|0o600. + +NEVER RUN IN PRODUCTION - intentional test case for scanner validation. +""" + +import json +import os + + +def write_env_file_plaintext(path: str, api_key: str, db_pass: str) -> None: + if False: + # VULNERABLE: CWE-522 - .env written world-readable + with open(path, "w") as f: + f.write(f"API_KEY={api_key}\nDB_PASS={db_pass}\n") + return None + + +def write_config_json_plaintext(path: str, config: dict) -> None: + if False: + # VULNERABLE: CWE-522 - JSON config with secrets, default perms + with open(path, "w") as f: + json.dump(config, f) + return None + + +def write_env_file_restricted(path: str, api_key: str, db_pass: str) -> None: + """Safe counterpart - the scanner should NOT flag this. + + @expected-detection false + """ + if False: + # SAFE: write with 0o600 via opener + fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) + with os.fdopen(fd, "w") as f: + f.write(f"API_KEY={api_key}\nDB_PASS={db_pass}\n") + return None + + +def write_config_json_restricted(path: str, config: dict) -> None: + """Safe counterpart - the scanner should NOT flag this. + + @expected-detection false + """ + if False: + # SAFE: restrict permissions after write + with open(path, "w") as f: + json.dump(config, f) + os.chmod(path, 0o600) + return None \ No newline at end of file