mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-18 04:29:27 +00:00
feat(taint): guards -- a validator or allowlist check makes a value safe where it held
Three of the four fixed-side alerts left in the independent population had one cause: the fix was a check, and checks were not modelled. - wger's fix is `url_has_allowed_host_and_scheme`: `if not ...(next_url): next_url = reverse(..)` and `if next_url and ...(next_url): return redirect`. - Ultimate Member's fix is an allowlist: `in_array(strtoupper($order), [...], true) ? $order : 'ASC'` and `elseif (in_array($sortby, $allowed, true))`. (The fourth, Signal K, is `assertAllowedHost`, a project-local function. It is not covered.) Sanitizer facts can now say `guard = true`. A guard is a check, never a cleansing call on a path. A flow is discharged only when a guard tests a variable the path carries, and the flow is in one of these places: - inside the branch whose condition requires the check. The guard may be combined with `&&` only; - past a failure test, a single `!` combined with `||` only, whose branch exits (return, exit, die, wp_die, throw) or overwrites the value, and whose condition dominates the flow's end; - through a ternary whose condition requires the check, where the other arm cannot carry the value. The engine can enter through the condition's own copy of the value, which never becomes the result. Facts: `in_array` (PHP) and Django's `url_has_allowed_host_and_scheme` / `is_safe_url` (Python). A row now consults guards even when a request lists no sanitizers. Two Joern-gated tests cover the discharged shapes and, as controls, an unguarded redirect, a guard on a different variable, a failure that is only logged, the unguarded else branch, and the ternary arm the check does not select. Both tests fail without the guard. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
dfc5e71730
commit
03f2ce7162
@@ -124,6 +124,8 @@ class TaintSpec:
|
||||
origin_anchors: tuple[str, ...] = ()
|
||||
# The sanitizers that protect only a value inside a quoted literal (`esc_sql`); a subset of `sanitizers`.
|
||||
quoted_sanitizers: tuple[str, ...] = ()
|
||||
# Checks that make a value safe where they held (see `SanitizerFact.guard`); never in `sanitizers`.
|
||||
guards: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -257,7 +259,8 @@ def taint_specs(
|
||||
{f"{call}:{fact.value_arg if call in fact.returns else 0}" for fact in scoped.dispatches if fact.returns for call in fact.apply}
|
||||
)
|
||||
)
|
||||
sanitizers = tuple(sorted({call for fact in scoped.sanitizers if not _is_shape(fact) for call in fact.calls}))
|
||||
sanitizers = tuple(sorted({call for fact in scoped.sanitizers if not _is_shape(fact) and not fact.guard for call in fact.calls}))
|
||||
guards = tuple(sorted({call for fact in scoped.sanitizers if fact.guard for call in fact.calls}))
|
||||
safe_shapes = tuple(sorted({call for fact in scoped.sanitizers if _is_shape(fact) for call in fact.calls}))
|
||||
quoted = tuple(sorted({call for fact in scoped.sanitizers if not _is_shape(fact) and fact.quoted_only for call in fact.calls}))
|
||||
by_family: dict[str, set[str]] = {}
|
||||
@@ -290,6 +293,7 @@ def taint_specs(
|
||||
fixed_origin=family_id in fixed_origin,
|
||||
origin_anchors=tuple(sorted(anchors.get(family_id, ()))),
|
||||
quoted_sanitizers=quoted,
|
||||
guards=guards,
|
||||
)
|
||||
for family_id, calls in sorted(by_family.items())
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ def request_params(
|
||||
"fixedOrigin": "true" if spec.fixed_origin else "false",
|
||||
"originAnchors": spec.origin_anchors,
|
||||
"quotedSanitizers": spec.quoted_sanitizers,
|
||||
"guards": spec.guards,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user