Files
Open_Ultra_SAST/.kiro/specs/authorization-obligations/research.md
T
Marius CiepluchandClaude Fable 5.1 27626d3a07 docs(authorization-obligations): approve requirements; design and research
Requirements approved by the maintainer in session. Design: closed
obligation facts (operations and dischargers), obligation shapes as a
second family in the mechanism store, sibling-set consistency and an
optional declared policy as the policy sources, a Dominance protocol
with an order-based default so the checker works function-local before
reachability and guard dominance land, findings at suspicion carrying
consistency_violation / declared_policy_violation / function_local as
data, reports, manifest and ranking fields, handler_context harvest
mode, leave-one-out per obligation kind, and an obligations lever with
pending policy clauses under the existing gate. All 44 acceptance
criteria traced.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
2026-09-05 16:42:58 +02:00

10 KiB
Raw Blame History

Research & Design Decisions

Summary

  • Feature: authorization-obligations
  • Discovery Scope: Extension of the semantic layer (facts, shapes, store, MAP proposers), the entry-point mapper, reports, the pair corpus, leave-one-out and the improve lever; complex integration because it consumes two unimplemented upstream specs.
  • Key Findings:
    • The absence pairs are real and structured. vampi-books-get-by-title has identical code on both sides except a constraint user=user bound from token_validator(request.headers.get('Authorization'))['sub']; the vulnerable branch queries Book by title alone. The difference is the provenance of a constraint, not a flow. 15 of 29 agent-vfc rows and 6 of the 19 non-teaching vibe-py pairs carry absence labels (missing_auth_guard, permissive_default, identity_from_request_body, secret_in_client, validation_strength).
    • The entry-point mapper already classifies every route as public, authenticated or role-restricted from its decorators (mapping._python_route_access), records the decorators as access_evidence, and lists conditions (if tests that look like checks). That is the sibling detector's upstream for path guards and needs no change.
    • FileIR binds carry value_text, names and call_name; variants.source_kind_of_name already traces a name through binds to a fact source, a parameter or a container read. Provenance of a constraint's value (authenticated context versus request input versus constant) is the same trace with one more source class.
    • MechanismStore rows carry a free shape dict and deterministic ids from Shape.key(); a second shape family fits without schema change. MechanismEdit, apply_mechanism_edits, StoreSnapshot, the tombstone and evaluate_mechanism_profiles are reusable as-is for a second lever family.
    • reachability-flow-model (unapproved) defines PathRecord(entry, hops, sink, sink_line, sink_node, incomplete_at, ...) and compose(graph, summaries, entry_nodes, max_depth). Obligations need paths from an entry point to an operation, which is a sink-shaped target; the same compose works when obligated operations are passed as the sink set. guard-dominance-regime has no spec directory yet; only the roadmap line exists. Dominance must therefore be consumed through a narrow interface this design defines and the two upstream specs later satisfy.
    • The evidence ladder is closed: OverlayRecord allows static_corroboration and suspicion; proof rungs are forbidden outside the sandbox. Obligation findings are StaticFindings at suspicion with the obligation label carried as data, so no ladder change is needed and Req 6.1 holds by construction.
    • The pair scorer counts promote and coverage-with-source records as detections; an obligation finding has no source or sink. Detection for obligation-labeled rows must be an additive rule keyed on the obligation label (allowed by the corpus spec's "one additive label field").

Research Log

What the absence pairs actually differ by

  • Context: Decide whether absence bugs can be a shape family at all.
  • Sources Consulted: benchmarks/pairs/vibe-py excerpts for missing_auth_guard and permissive_default rows; benchmarks/pairs/agent-vfc/recipes.toml mechanisms; mechanisms.toml definitions.
  • Findings: three recurring differences: (a) a constraint added to a data access whose value comes from the authenticated context; (b) a guard call or decorator added before the operation (early return on a failed token check, login_required); (c) a literal changed from permissive to restrictive (CORS(app) → origins list, DEBUG=True → False, RLS enabled). A fourth, rarer: identity read from the body replaced by identity from the token (same as (a) seen from the other side).
  • Implications: discharger kinds identity_constraint, path_guard, ownership_check, non_permissive_value, validated_input; the value provenance is the discriminating feature for (a) and (d).

Where the policy comes from

  • Context: Absence is relative to intent; the tool must not invent intent.
  • Sources Consulted: mapping.analyze_entry_points (routes, access level, evidence), Real-Vuln repositories (many handlers per resource), Clearwing's sibling-comparison practice.
  • Findings: in every Real-Vuln web app the handlers on one resource share a router and a model; the vulnerable handler is the odd one out. Route access classification already exists. A declared policy is the only way to say "this route is meant to be public"; the hunter can only guess it.
  • Implications: consistency first (no declaration needed, measurable now), declared policy second (stronger rung), hunter last (records intent, never raises).

Dominance before guard-dominance-regime exists

  • Context: Req 5 needs "a discharger dominates the path"; the spec that owns dominance is not written.
  • Findings: within one handler, the existing FunctionIR order of binds and calls gives statement order; a guard call followed by an early return before the operation line is dominance in the common case; decorators dominate the whole handler; middleware attached to the router dominates every route under it. Across calls, PathRecord.hops gives the sequence of nodes; a discharger on any hop before the operation node dominates the path when the hop's function returns early on failure.
  • Implications: define Dominance as a narrow protocol (dominates(witness, operation) -> bool) with a built-in OrderDominance implementation (statement order in the handler, decorators, router middleware, hop order) that guard-dominance-regime may replace with nesting-aware dominance. The degraded mode (function_local) is OrderDominance without path records.

Store and lever reuse

  • Context: Avoid a second store and a second lever machinery.
  • Findings: Mechanism.shape is a dict validated where used; corpus_mechanism_id hashes shape.key(); validate_mechanism checks guard and source kinds against variants constants. A second family needs its own closed kinds check and a family field in the shape dict; the tombstone, snapshot and profile clause need nothing.
  • Implications: ObligationShape.to_dict() carries family = "obligation"; the lever validator dispatches on family; search_tree ignores obligation rows (they are not sink shapes) and the obligation checker ignores sink rows.

Architecture Pattern Evaluation

Option Description Strengths Risks / Limitations Notes
Absence as a taint sink ("sink with a missing sanitizer") Treat protected access as a sink needing an identity "sanitizer" Reuses taint Wrong semantics: the constraint is not a sanitizer of a flow; unconstrained access has no tainted argument Rejected
Model-authored authorization rules LLM writes per-project rules Flexible Violates no-model-authored-patterns; unverifiable Rejected
Obligations with dischargers, policy from consistency plus declaration, path-aware Closed facts, learned shapes, sibling anomaly, optional declared policy, dominance protocol Zero-dep, measurable leave-one-out, honest rungs, grows with corpus Coarse resource grouping; dominance approximated until the guard spec lands Selected
Framework-specific authorization linters Per-framework hard-coded checks High precision Regex growth, no learning, no policy Rejected

Design Decisions

Decision: Obligation findings are suspicion with a data label, not new ladder rungs

  • Context: Req 6.1 names three labels; the ladder is closed and owned elsewhere.
  • Selected Approach: StaticFinding.evidence_level = "suspicion", tags obligation:<kind>, discharger:<kind>, obligation_evidence:<label>; reports render the label. Dispositions and ALLOWED_EVIDENCE unchanged.
  • Trade-offs: SARIF consumers see suspicion plus properties; honest and non-invasive.

Decision: Dominance is a protocol with an order-based default

  • Context: guard-dominance-regime is unwritten; this spec cannot wait for it to be useful.
  • Selected Approach: Dominance protocol; OrderDominance default (decorator, router middleware, statement order before the operation, hop order along a path). The guard spec replaces it without touching callers.
  • Trade-offs: order-based dominance misses guards inside conditionals; recorded as the known coarseness and measured on the fixed twins.

Decision: Policy is never written by the tool

  • Selected Approach: declared policy is a hand-maintained file with a closed schema; the lever may only propose a clause, journaled as pending until a human accepts; scans never read pending clauses.

Decision: Resource identity for sibling grouping

  • Selected Approach: a handler's resource is the trailing segment of its route path with parameters stripped, joined with the operation's target token (model, table, collection name from the operation fact match); router or module is the second key. Coarse but deterministic and text-free.

Risks & Mitigations

  • Sibling grouping over-merges unrelated handlers → minimum sibling count, both keys required, siblings listed in the finding for review.
  • Consistency leaks on legitimately public handlers → declared public routes suppress path-guard violations; leak rate on fixed twins reported per slice; the lever's holdout gate rejects shapes that leak.
  • Order dominance misses conditional guards → function_local and consistency_violation labels stay below proof; guard spec replaces the default.
  • Corpus excerpts lack registration context → new handler_context harvest mode; pointer pairs fetch whole files anyway.
  • Two unimplemented upstream specs → this design degrades to function-local mode without path records, so tasks 1–6 are implementable before reachability lands; only path-aware evaluation waits.

References

  • .kiro/specs/reachability-flow-model/design.md (PathRecord, compose)
  • .kiro/specs/corpus-seeded-mechanisms/design.md (store, lever, LOO)
  • src/openultrasast/mapping.py (EntryPointRecord, _python_route_access)
  • src/openultrasast/semantic/variants.py (source_kind_of_name, classify_guard)