Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions docs/VULNERABILITY_CATALOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 |
Expand All @@ -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 |
Expand Down
66 changes: 62 additions & 4 deletions vulns/VULNERABILITY_CATALOG.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -41,7 +41,9 @@
"CWE-256",
"CWE-295",
"CWE-321",
"CWE-327",
"CWE-330",
"CWE-338",
"CWE-346",
"CWE-347",
"CWE-352",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
50 changes: 50 additions & 0 deletions vulns/python/insecure-random-token.py
Original file line number Diff line number Diff line change
@@ -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
64 changes: 64 additions & 0 deletions vulns/python/weak-crypto-md5.py
Original file line number Diff line number Diff line change
@@ -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
Loading