diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index 0166d67..5763d34 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:** 60 -- **Expected detections:** 60 -- **`VULNERABLE:` markers:** 114 (individual lines a scanner should flag) -- **`SAFE:` markers:** 68 (lines a scanner must not flag — the false-positive control group) +- **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) - **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text -- **CWE categories:** 44 — 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-330, 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:** 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 ## How coverage is scored @@ -74,6 +74,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Excessive user-record exposure in an API response | [`excessive-data-exposure.py`](../vulns/python/excessive-data-exposure.py) | CWE-201 | high | yes | 1 vuln / 1 safe | | Flask debug mode enabled in application configuration | [`flask-debug-enabled.py`](../vulns/python/flask-debug-enabled.py) | CWE-489 | high | yes | 1 vuln / 1 safe | | Hardcoded secret used to configure session signing | [`hardcoded-session-secret.py`](../vulns/python/hardcoded-session-secret.py) | CWE-798 | high | yes | 1 vuln / 1 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 | | LDAP injection via unescaped search filter | [`ldap-injection.py`](../vulns/python/ldap-injection.py) | CWE-90 | high | yes | 1 vuln / 1 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 | @@ -87,6 +88,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Server-side request forgery via requests.get on user-supplied URL | [`ssrf-requests-user-url.py`](../vulns/python/ssrf-requests-user-url.py) | CWE-918 | high | yes | 2 vuln / 1 safe | | 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 | | TOCTOU race condition in file access | [`toctou-race-condition.py`](../vulns/python/toctou-race-condition.py) | CWE-362 | medium | yes | 3 vuln / 2 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 | | 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 | diff --git a/vulns/VULNERABILITY_CATALOG.json b/vulns/VULNERABILITY_CATALOG.json index 04ef479..153fdaf 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": 60, - "expected_detections": 60, - "vulnerable_markers": 114, - "safe_markers": 68, + "test_cases": 62, + "expected_detections": 62, + "vulnerable_markers": 123, + "safe_markers": 73, "languages": [ "dotenv", "go", @@ -41,7 +41,9 @@ "CWE-256", "CWE-295", "CWE-321", + "CWE-327", "CWE-330", + "CWE-338", "CWE-346", "CWE-347", "CWE-352", @@ -858,6 +860,34 @@ 23 ] }, + { + "id": "py-insecure-random-token", + "file": "vulns/python/insecure-random-token.py", + "title": "Security tokens derived from a predictable PRNG", + "category": "python", + "language": "python", + "cwe": "CWE-338", + "cwes": [ + "CWE-338" + ], + "severity": "high", + "expected_detection": true, + "description": "Password-reset tokens, session identifiers and MFA codes are drawn", + "detection_target": "Use of the `random` module (or a time-derived seed) on a value", + "safe_guard": "The entire fixture is wrapped in `if False:` and cannot execute. It", + "attribution": "line", + "vulnerable_lines": [ + 34, + 35, + 38, + 41 + ], + "safe_lines": [ + 44, + 47, + 50 + ] + }, { "id": "py-insecure-temp-file", "file": "vulns/python/insecure-temp-file.py", @@ -1175,6 +1205,34 @@ 74 ] }, + { + "id": "py-weak-crypto-md5", + "file": "vulns/python/weak-crypto-md5.py", + "title": "Weak cryptographic algorithms for integrity and confidentiality", + "category": "python", + "language": "python", + "cwe": "CWE-327", + "cwes": [ + "CWE-327" + ], + "severity": "high", + "expected_detection": true, + "description": "Broken primitives are used where a security guarantee is claimed:", + "detection_target": "Selection of a broken hash or cipher primitive at the call", + "safe_guard": "The entire fixture is wrapped in `if False:` and cannot execute. It", + "attribution": "line", + "vulnerable_lines": [ + 37, + 41, + 50, + 54, + 58 + ], + "safe_lines": [ + 45, + 62 + ] + }, { "id": "py-weak-password-hash", "file": "vulns/python/weak-password-hash.py", diff --git a/vulns/python/insecure-random-token.py b/vulns/python/insecure-random-token.py new file mode 100644 index 0000000..d123c29 --- /dev/null +++ b/vulns/python/insecure-random-token.py @@ -0,0 +1,50 @@ +""" +@id py-insecure-random-token +@test-case Security tokens derived from a predictable PRNG +@cwe CWE-338 +@severity high +@language python +@expected-detection true +@description Password-reset tokens, session identifiers and MFA codes are drawn + from `random`, a Mersenne Twister seeded from the clock. It is not + a cryptographic PRNG: observing 624 consecutive outputs recovers the + internal state, after which every future token is predictable, so an + attacker can mint a valid reset token for another account. +@safe-guard The entire fixture is wrapped in `if False:` and cannot execute. It + performs no file, network, or process operations and issues no token + to anything. +@detection-target Use of the `random` module (or a time-derived seed) on a value + that is used as a credential, rather than `secrets` / + `os.urandom`. The sink is the security role of the value, not + the randomness call in isolation. + +NEVER RUN IN PRODUCTION - intentional test case for scanner validation. +""" + + +if False: + import random + import secrets + import string + import time + + ALPHABET = string.ascii_letters + string.digits + + def generate_password_reset_token() -> str: + random.seed(int(time.time())) # VULNERABLE: CWE-338 seed is guessable to the second + return "".join(random.choice(ALPHABET) for _ in range(32)) # VULNERABLE: CWE-338 Mersenne Twister is not a CSPRNG + + def generate_session_id() -> str: + return "%032x" % random.getrandbits(128) # VULNERABLE: CWE-338 predictable once state is recovered + + def generate_mfa_code() -> str: + return str(random.randint(100000, 999999)) # VULNERABLE: CWE-338 MFA code from a non-cryptographic PRNG + + def generate_password_reset_token_safe() -> str: + return secrets.token_urlsafe(32) # SAFE: secrets draws from the OS CSPRNG + + def generate_session_id_safe() -> str: + return secrets.token_hex(16) # SAFE: os.urandom-backed + + def generate_mfa_code_safe() -> str: + return str(secrets.randbelow(900000) + 100000) # SAFE: uniform draw from the CSPRNG diff --git a/vulns/python/weak-crypto-md5.py b/vulns/python/weak-crypto-md5.py new file mode 100644 index 0000000..1df675f --- /dev/null +++ b/vulns/python/weak-crypto-md5.py @@ -0,0 +1,64 @@ +""" +@id py-weak-crypto-md5 +@test-case Weak cryptographic algorithms for integrity and confidentiality +@cwe CWE-327 +@severity high +@language python +@expected-detection true +@description Broken primitives are used where a security guarantee is claimed: + MD5 and SHA-1 for artifact integrity (both have practical chosen- + prefix collisions), DES and RC4 for confidentiality (56-bit key, + biased keystream), and AES in ECB mode, which leaks plaintext + structure because identical blocks encrypt identically. +@safe-guard The entire fixture is wrapped in `if False:` and cannot execute. It + performs no file, network, or process operations, and every key is a + fixed synthetic literal that protects nothing. +@detection-target Selection of a broken hash or cipher primitive at the call + site — hashlib.md5/sha1 used for integrity, DES/ARC4 + construction, and AES with MODE_ECB — rather than any taint + flow from user input. + +NEVER RUN IN PRODUCTION - intentional test case for scanner validation. +""" + + +if False: + import hashlib + import hmac + + from Crypto.Cipher import AES, ARC4, DES # pinned to nothing installable + + # Fixed synthetic key material. Generated to be committed here, secures nothing. + DEMO_KEY_8 = b"8bytekey" + DEMO_KEY_16 = b"0123456789abcdef" + + def verify_release_artifact(artifact: bytes, expected_digest: str) -> bool: + """Integrity check of a downloaded artifact.""" + digest = hashlib.md5(artifact).hexdigest() # VULNERABLE: CWE-327 MD5 has chosen-prefix collisions + return digest == expected_digest + + def verify_release_artifact_sha1(artifact: bytes, expected_digest: str) -> bool: + digest = hashlib.sha1(artifact).hexdigest() # VULNERABLE: CWE-327 SHA-1 collisions are practical (SHAttered) + return digest == expected_digest + + def verify_release_artifact_safe(artifact: bytes, expected_digest: str) -> bool: + digest = hashlib.sha256(artifact).hexdigest() # SAFE: SHA-256 has no known collision attack + return hmac.compare_digest(digest, expected_digest) + + def encrypt_session_blob(plaintext: bytes) -> bytes: + """Confidentiality for a stored session blob.""" + cipher = DES.new(DEMO_KEY_8, DES.MODE_ECB) # VULNERABLE: CWE-327 DES 56-bit key is brute-forceable + return cipher.encrypt(plaintext) + + def encrypt_stream_token(plaintext: bytes) -> bytes: + cipher = ARC4.new(DEMO_KEY_16) # VULNERABLE: CWE-327 RC4 keystream is measurably biased + return cipher.encrypt(plaintext) + + def encrypt_record_ecb(plaintext: bytes) -> bytes: + cipher = AES.new(DEMO_KEY_16, AES.MODE_ECB) # VULNERABLE: CWE-327 ECB leaks plaintext block structure + return cipher.encrypt(plaintext) + + def encrypt_record_safe(plaintext: bytes) -> tuple[bytes, bytes, bytes]: + cipher = AES.new(DEMO_KEY_16, AES.MODE_GCM) # SAFE: authenticated encryption with a per-message nonce + ciphertext, tag = cipher.encrypt_and_digest(plaintext) + return cipher.nonce, ciphertext, tag