mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
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
This commit is contained in:
co-authored by
Claude Opus 5
parent
0fcd5feade
commit
d15985f695
@@ -175,3 +175,49 @@ def test_constraint_params_are_not_dischargers_on_their_own() -> None:
|
||||
assert params, "the fixture would be vacuous if the facts carried no constraint params"
|
||||
assert not (params & set(spec.dischargers)), f"constraint params must not discharge alone: {sorted(params & set(spec.dischargers))}"
|
||||
assert "current_user" in spec.dischargers, "identity sources still do discharge"
|
||||
|
||||
|
||||
def test_configuration_facts_do_not_become_authorization_obligations() -> None:
|
||||
"""contributor-scan 2.6. A permissive CORS origin is discharged by a VALUE, not by a guard.
|
||||
|
||||
Taking every obligation fact made `app.run(...)` an operation requiring an authorization check, and put
|
||||
thirteen tokens in both the operation and the discharger list -- an obligation that is its own discharge,
|
||||
the same shape as the taint sink that used to sanitize itself.
|
||||
"""
|
||||
from openultrasast.model.specs import dominance_specs
|
||||
|
||||
spec = dominance_specs(language="python")["access_control"]
|
||||
|
||||
assert not set(spec.operations) & set(spec.dischargers), "nothing may be both the obligation and its discharge"
|
||||
assert "app.run" not in spec.operations
|
||||
assert "CORS" not in spec.operations
|
||||
|
||||
|
||||
def test_input_validation_does_not_discharge_an_authorization_obligation() -> None:
|
||||
"""`jsonschema.validate` establishes the shape of a request, never who is making it."""
|
||||
from openultrasast.model.specs import dominance_specs
|
||||
|
||||
spec = dominance_specs(language="python")["access_control"]
|
||||
|
||||
assert "validate" not in spec.dischargers
|
||||
assert "parse_obj" not in spec.dischargers
|
||||
assert "token_validator" in spec.dischargers, "a real route guard must survive the filter"
|
||||
|
||||
|
||||
def test_a_guard_discharges_only_the_obligation_it_is_a_guard_of() -> None:
|
||||
"""The relation the facts state, which the flat list was throwing away.
|
||||
|
||||
`filter_by` requires an identity_constraint or an ownership_check; `token_validator` is a path_guard.
|
||||
Pooled together, authenticating the caller discharged an object-level obligation, so a handler that
|
||||
authenticates and then looks the record up by a PATH parameter read as fully guarded -- VAmPI's broken
|
||||
object-level authorization, and the shape of most real IDORs.
|
||||
"""
|
||||
from openultrasast.model.specs import dominance_specs
|
||||
|
||||
spec = dominance_specs(language="python")["access_control"]
|
||||
|
||||
assert spec.requirements["filter_by"] == ("identity_constraint", "ownership_check")
|
||||
identity = set(spec.dischargers_by_kind["identity_constraint"])
|
||||
assert "token_validator" not in identity, "authentication is not an object-level check"
|
||||
assert "token_validator" in set(spec.dischargers_by_kind["path_guard"])
|
||||
assert "['sub']" in identity or '["sub"]' in identity
|
||||
|
||||
Reference in New Issue
Block a user