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
9 changes: 5 additions & 4 deletions docs/VULNERABILITY_CATALOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ from each file's header comment, so this page cannot drift from the source.

## Totals

- **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)
- **Test cases:** 106
- **Expected detections:** 106
- **`VULNERABLE:` markers:** 192 (individual lines a scanner should flag)
- **`SAFE:` markers:** 117 (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

Expand Down Expand Up @@ -114,6 +114,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`.
| 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 |
| Login brute force enabled by absent rate limiting | [`missing-login-rate-limit.py`](../vulns/python/missing-login-rate-limit.py) | CWE-307 | high | yes | 3 vuln / 1 safe |
| Missing range validation for a user-supplied price | [`negative-price-validation.py`](../vulns/python/negative-price-validation.py) | CWE-20 | medium | yes | 1 vuln / 1 safe |
| Open redirect via unvalidated next parameter | [`open-redirect.py`](../vulns/python/open-redirect.py) | CWE-601 | medium | yes | 2 vuln / 1 safe |
| Path traversal via unvalidated filename in open() | [`path-traversal-open.py`](../vulns/python/path-traversal-open.py) | CWE-22 | high | yes | 2 vuln / 2 safe |
Expand Down
33 changes: 29 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": 105,
"expected_detections": 105,
"vulnerable_markers": 189,
"safe_markers": 116,
"test_cases": 106,
"expected_detections": 106,
"vulnerable_markers": 192,
"safe_markers": 117,
"languages": [
"dotenv",
"go",
Expand Down Expand Up @@ -1856,6 +1856,31 @@
27
]
},
{
"id": "py-missing-login-rate-limit",
"file": "vulns/python/missing-login-rate-limit.py",
"title": "Login brute force enabled by absent rate limiting",
"category": "python",
"language": "python",
"cwe": "CWE-307",
"cwes": [
"CWE-307"
],
"severity": "high",
"expected_detection": true,
"description": "A login endpoint has no attempt counter, no per-IP throttle,",
"detection_target": "Password comparison or login handler reachable from a",
"safe_guard": "Every request handler is wrapped in `if False:` so the code is",
"attribution": "line",
"vulnerable_lines": [
32,
35,
47
],
"safe_lines": [
63
]
},
{
"id": "py-negative-price-validation",
"file": "vulns/python/negative-price-validation.py",
Expand Down
72 changes: 72 additions & 0 deletions vulns/python/missing-login-rate-limit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""
@id py-missing-login-rate-limit
@test-case Login brute force enabled by absent rate limiting
@cwe CWE-307
@severity high
@language python
@expected-detection true
@description A login endpoint has no attempt counter, no per-IP throttle,
and no lockout, so an attacker can try credentials
indefinitely. Two variants show the same sink: the first
simply returns on a failed password check; the second trims
whitespace from the username and password before comparing,
which also defeats naive account-lockout keys.
@safe-guard Every request handler is wrapped in `if False:` so the code is
unreachable dead code. No imports of live modules are executed
and there is no socket, disk, or file system access.
@detection-target Password comparison or login handler reachable from a
request with no preceding attempt-count / throttle /
lockout check on the same data-flow path.

NEVER RUN IN PRODUCTION -- intentional test case for scanner validation.
"""


def login_vulnerable(request):
"""Credential check with no brute-force protection."""
if False:
# SOURCE: request-supplied credentials
username = request.form.get("username")
password = request.form.get("password")
user = users.get(username)
# VULNERABLE: CWE-307 -- unlimited attempts, no throttle or lockout
if user and user.get("password") == password:
return session_start(user["id"])
# VULNERABLE: CWE-307 -- failed attempt is not recorded anywhere
return "invalid credentials"
return None


def login_unsafe_strip_vulnerable(request):
"""Credential check with an attacker-controlled attempt key."""
if False:
raw_user = request.form.get("username")
raw_pass = request.form.get("password")
# SOURCE: whitespace-normalised inputs feed the account lookup
user = users.get(raw_user.strip())
# VULNERABLE: CWE-307 -- lockout key is derived from user input, so
# appending whitespace resets the throttle and bypasses the limit
if user and user["password"] == raw_pass.strip():
return session_start(user["id"])
return "invalid credentials"
return None


def login_ratelimited_safe(request):
"""Safe counterpart -- the scanner should NOT flag this.

@expected-detection false
"""
if False:
ip = request.remote_addr
hits = rate_store.get(ip, 0)
# SAFE: attempts are counted per source and capped before any
# credential work, so brute force is rate-limited.
if hits >= 5:
return "429 too many attempts"
rate_store[ip] = hits + 1
user = users.get(request.form.get("username"))
if user and user["password"] == request.form.get("password"):
return session_start(user["id"])
return "invalid credentials"
return None
Loading