Files
Marius CiepluchandClaude Opus 5 d15985f695 fix(model): a guard discharges the obligation it is a guard of
contributor-scan 2.6. Access control entailed 7 of 9 VAmPI handlers,
including three that do authorize and three the OpenAPI spec declares
public. Five separate defects, each invisible to a corpus of one-function
excerpts and each found by asking the query what it actually matched.

**A trailing segment is not a match.** `query.get` was matched by its last
segment, so every `.get(` in the repository became a protected database
operation -- `request.headers.get('Authorization')` and
`request_data.get('username')` included. Four of the seven false positives
came from this, and so did the apparent detection of the known BOLA: it was
entailed because fetching the Authorization header had no dominating guard,
not because of the defect. A qualified token now requires its qualifier.

**Configuration is not authorization.** dominance_specs took every
obligation fact. `security_setting` operations are discharged by a VALUE,
which config_specs already claims them for; pooled in here they made
`app.run(...)` an operation requiring an authorization guard and put
thirteen tokens in BOTH lists -- an obligation that is its own discharge,
the same shape as the taint sink that used to sanitize itself.

**Input validation is not authorization.** `jsonschema.validate` was
counted as a guard, so handlers with no authorization check at all read as
guarded. No operation in the table requires validated_input.

**A guard discharges the obligation it is a guard of.** The largest one.
`orm-read` requires an identity_constraint or an ownership_check;
`token_validator` is a path_guard. Flattened into one list, authenticating
the caller discharged an object-level obligation -- a handler that
authenticates and then looks the record up by a PATH parameter read as
fully guarded. The facts state that relation and the spec was discarding
it; it is now carried and the engine asks per operation.

**A word boundary needs a word.** Found while fixing the above. Guard
matching wrapped every token in `(?<![A-Za-z0-9_])...(?![A-Za-z0-9_])`.
The identity token `['sub']` begins with `[`, and the character before it
in `resp['sub']` is `p` -- so the assertion could never hold. Every
identity token starting with punctuation was unmatchable, and the identity
constraint that separates a correct lookup from an IDOR was invisible.

Also: dominance rows were repository-wide, so the model's unguarded
`delete_user` was attributed to the API handler of the same name. Rows are
scoped to the region's file, which is what the rung's own justification
says -- the evidence is a file contradicting itself.

Measured against VAmPI's OpenAPI `security` blocks, which declare per
operation whether auth is required, and agree with the code:

  vampi precision   28.6% (2/7) -> 50.0% (2/4)
  known found       3 of 3, all model_entailed

And on the labelled access-control pairs, which nobody tuned this against:

  all 9 pairs       pair_correct 3 -> 3, leak 44.4% -> 11.1%
  7 non-VAmPI       pair_correct 2 -> 2, leak 42.9% -> 0.0%

Every detection lost was on a pair whose FIXED side was flagged too, which
scores nothing. No correct detection was traded for the precision.

get_by_title was flagged before it was in the recipe: its code carries the
comment "# Broken Object Level Authorization" and the README lists it, so a
real finding was being scored as a false positive. It is now the recipe's
third known vulnerability.

Both remaining false positives are endpoints the spec declares public.
Recorded as 2.8, with the reason the mapper's access_level cannot simply be
trusted: for decorator frameworks "public" means "no decorator found",
which is the bug itself.

Gates byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
2026-09-09 11:02:48 +02:00

62 lines
2.8 KiB
TOML

# VAmPI -- a deliberately vulnerable Flask API, small enough to iterate on.
#
# This is the DETECTION checkout. The corpus already scores excerpts cut out of VAmPI, and one of them
# (users/update_password) was diagnosed as a miss. Scanning the whole repository asks the question the
# excerpts cannot: does a verdict survive when the route registration, the handler and the query live in
# three different modules, and when the region ranker has to find the handler rather than being handed it?
#
# Its routes are registered through an OpenAPI `operationId`, not a decorator, so it also exercises the
# registration-based entry-point mapping rather than the decorator path.
name = "vampi"
url = "https://github.com/erev0s/VAmPI.git"
commit = "f16052dce83f05847133ec98f01c5193a41de7d8"
language = "python"
license = "MIT"
license_file = "LICENSE"
measures = ["detection"]
note = """
520 lines across 11 Python files: too small to say anything about the envelope, and chosen for exactly that
reason -- a detection result here is not confounded by budget exhaustion or region ranking.
"""
# VAmPI serves the vulnerable code paths behind a global switch, `int(os.getenv('vulnerable', 1))` in app.py.
# The default is already vulnerable; it is pinned here because a measurement that silently ran the patched
# branch would report a clean repository and look like a detection failure.
[environment]
vulnerable = "1"
[[known]]
id = "VAmPI-SQLI"
family = "injection"
weakness = "SQL injection: the username is interpolated into the query string at line 72 and executed at 73"
file = "models/user_model.py"
function = "get_user"
line = 73
reference = "README.md 'List of Vulnerabilities'; the vulnerable branch is guarded by `if vuln:`"
in_scope = true
[[known]]
id = "VAmPI-BOLA"
family = "access_control"
weakness = "unauthorized password change: the path username is used instead of the authenticated token subject"
file = "api_views/users.py"
function = "update_password"
line = 187
reference = "README.md 'Unauthorized Password Change'; the non-vulnerable branch uses resp['sub']"
in_scope = true
# Found by the engine before it was in this file. The first repository run flagged get_by_title, and the code
# says why in its own comment: the vulnerable branch fetches a book by title with no ownership constraint and
# returns its secret. It is listed in the README alongside the password one, and leaving it out of the recipe
# would have scored a real finding as a false positive.
[[known]]
id = "VAmPI-BOLA-BOOKS"
family = "access_control"
weakness = "broken object level authorization: a book is fetched by title with no ownership constraint and its secret returned"
file = "api_views/books.py"
function = "get_by_title"
line = 51
reference = "the code comment '# Broken Object Level Authorization' on the vulnerable branch; README 'Broken Object Level Authorization'"
in_scope = true