#Auth analysis
Rust is the stable target. Python and Go have shipped precision work as of 0.7.0 (FastAPI cross-file dependencies, Go DAO-helper filtering, same-file caller-scope IPA) and are usable on real codebases. Ruby, Java, JavaScript, and TypeScript have rule scaffolding in src/auth_analysis/config.rs but no benchmark corpus yet; treat findings there as preview.
#What it catches
The Rust rule is rs.auth.missing_ownership_check. It fires when a request handler reaches a privileged operation that takes a scoped identifier (*_id, row reference, scoped resource) without a preceding ownership or membership check.
Concretely, it looks for these patterns of authorization in the function body and flags the call when none are present:
- A call to a recognised authorization helper. Defaults:
check_ownership,has_ownership,require_ownership,ensure_ownership,is_owner,authorize,verify_access,has_permission,can_access,can_manage, plus*_membershipandrequire_{group,org,workspace,tenant,team}_membervariants. Extend in[analysis.languages.rust]. - An ownership-equality check on a row reference:
if owner_id != user.id { return 403 }or anyfield_id != self_actorshape. The check writesAuthCheckevidence back to the row-fetch arguments viaAnalysisUnit.row_field_vars. - A self-actor reference:
let user = require_auth(...).await?followed by use ofuser.id,user.user_id,user.uid. The actor is recognised from typed extractor params (Extension<Session>,CurrentUser, etc.) and from typed helper bindings. - A typed extractor wrapper that proves route-level capability/policy enforcement: meilisearch-style
GuardedData<ActionPolicy<X>, _>. Recognised by outer wrapper name (last segment, case-insensitivestarts_with) soGuardedData<ActionPolicy<X>, Data<AuthController>>is classified by the outerGuardedData, not by whether an inner generic arg substring-matchesauth. Configured viapolicy_guard_names(Rust default:["Guarded"]). Distinct from authentication-only wrappers so the pattern doesn't pollute regular call recognition. - A SQL query that joins through an ACL table or filters by
user_idpredicate. Detected without a SQL parser viasql_semantics.rs; the authorized result variable propagates throughlet row = ...prepare(LIT)...,for row in result,let id = row.get(...). - A helper-summary lift: handler calls
validate_target(db, widget_id, user.id)whose body contains arequire_*_membercall. Cross-function summaries are merged at fixed-point (capped at 4 iterations).
Handlers registered through attribute macros (#[get("/path")], #[routes::path(…)]) or external service-config builders are also walked for typed-extractor guards, complementing the .route(...) registration path.
#Caller-scope-entity exemption
<entity>.id / <entity>.pk is not flagged when <entity> is a unit parameter named after a multi-tenant scope primitive: organization / org, project, team, workspace, tenant, account, community, group, repository / repo, company. The argument represents the caller's scope, not a user-controlled target, so internal helpers like def get_environments(request, organization): Environment.objects.filter(organization_id=organization.id, …) inherit the caller's authorization. Other field names (.name, .slug) still flag, and user / member / actor are deliberately excluded; those are handled by the actor-context recogniser.
#Project-level web-framework gate (Rust)
In Rust, the context_inputs and param-name arms of the user-input heuristic are gated by a project-level web-framework signal. The signal is three-valued:
Some(true): the project'sCargo.tomlnamesaxum,actix-web, orrocket, OR the file directly imports one (axum::,actix_web::,rocket::,axum_extra::). Heuristics stay on.Some(false):Cargo.tomlwas inspected and named no web framework, AND the file does not directly import one. Heuristics off; onlyRouteHandlerclassification (concrete route-registration evidence) survives.None: no detection ran (single-file scan with no project root). Heuristics on; behavior unchanged.
This avoids a class of FPs in non-web Rust crates where a debug-session handle named session would trip on session.update(cx, …)-style desktop-app code. Other languages keep prior behavior; the gate is currently Rust-only.
#Python: FastAPI cross-file dependencies
FastAPI's include_router chain is resolved across files. A child router declared in routes/task_instances.py and attached on a parent in routes/__init__.py inherits the parent's dependencies=[...].
- Module-level
router = APIRouter(dependencies=[Security(...)])is pre-walked once per file and merged onto every@<router>.<verb>(...)route attached in the same file. <parent>.include_router(<child_module>.<child_var>)edges are captured per file in pass 1, persisted intoGlobalSummaries::router_facts_by_module, and lifted onto the active file'sAuthorizationModel::cross_file_router_depsat pass 2 entry. Transitive lifts (grandparent to parent to child) iterate to fixpoint.Security(callable, scopes=[...])is recognised distinctly fromDepends(callable)and promotes the syntheticAuthChecktoAuthCheckKind::Other(route-level scope-checked authorization). BareDepends(callable)is still a Login-only check.
Module identity is the file basename without .py. This is sufficient for airflow-style task_instances.router naming; a project with two files of the same name in different subtrees will currently collide.
#Go: DAO-helper id-scalar precision pass
For non-route Go units, a parameter whose declared type is a bounded primitive scalar (int64, uint32, string, bool, byte, rune, float64, etc.) and whose name is id-shaped (id, *Id, *_id, *ids) is dropped from unit.params before ownership-check evaluation.
Real Go HTTP handlers always carry a framework-request-typed param (*http.Request, *gin.Context, echo.Context, *fiber.Ctx); per-framework route extractors set include_id_like_typed=true so id-shaped path params survive on real routes. The filter only fires when the unit was not classified as a route handler, so helpers like func GetRunByRepoAndID(ctx, repoID, runID int64) are recognised as DAO callees and the ownership check is expected at the calling route handler, not inside the helper.
#Same-file caller-scope IPA
When a private helper is called only from authorized route handlers in the same file, the caller's auth checks lift onto the helper as synthetic is_route_level=true AuthCheck entries.
- Iterated to a small fixpoint so transitive chains (route to mid_helper to leaf_helper) are covered.
- Refuses to authorize helpers with no in-file caller, helpers called from a mix of authorized and unauthorized callers, and helpers called only from un-lifted helpers.
- Cross-file equivalent is deferred.
This closes the FastAPI / Django / Flask shape where a route authenticates via decorator or dependency, then delegates to a private helper that performs the sink.
#Sink classification
The same call name can be safe on a local collection and dangerous on a database. The detector categorises each candidate sink before deciding whether to flag:
| Class | Examples | Default treatment |
|---|---|---|
InMemoryLocal |
map.insert, set.insert, vec.push on tracked local |
Never a sink |
RealtimePublish |
realtime.publish_to_group, pubsub.send |
Sink unless ownership is established for the channel scope |
OutboundNetwork |
http.post, reqwest::Client::post |
Sink unless a sanitiser is on the path |
CacheCrossTenant |
redis.set, memcached.set with scoped keys |
Sink unless tenant is checked |
DbMutation |
db.insert, repo.save with scoped IDs |
Sink unless ownership is established |
DbCrossTenantRead |
db.query returning rows from a tenant scope |
Sink unless ACL-join or tenant predicate is present |
Receiver type drives the classification when SSA type facts are available, so client.send(...) correctly resolves through the receiver's inferred type.
#What it can't catch
- Non-Rust frameworks, in practice. Scaffolding exists; coverage doesn't.
- Type-system authorization. A typestate pattern that makes unauthenticated handlers fail to compile (
fn endpoint(user: AuthenticatedUser<Admin>)) is invisible. This is mostly fine because the type system already enforced the check, but the rule won't credit it. - Authorization performed only via macros that the AST doesn't expose as a recognisable call.
- Cross-async-boundary actor binding. If the handler awaits
let user = require_auth(...).await?and then spawns a task that usesuser.idafter atokio::spawn, the spawn body is treated as a separate scope.
#The taint-based variant
A second rule, rs.auth.missing_ownership_check.taint, folds the same logic into the SSA/taint engine using the Cap::UNAUTHORIZED_ID capability (bit 12). Request-bound handler parameters seed UNAUTHORIZED_ID into taint state; ownership checks act as sanitizers that strip the cap; sinks that take scoped IDs require it absent.
This path is off by default while the standalone analyser carries the stable signal. Enable both:
[scanner]
enable_auth_as_taint = true
Run them together; if both fire for the same site, treat it as the same finding (the taint variant carries fuller flow evidence).
#Tuning
#Add a project-specific authorization helper
[[analysis.languages.rust.rules]]
matchers = ["require_subscription", "ensure_paid_seat"]
kind = "sanitizer"
cap = "unauthorized_id"
The same rule recognised in the standalone analyser also strips Cap::UNAUTHORIZED_ID for the taint-based variant.
#Add a project-specific typed-extractor policy wrapper
[analysis.languages.rust.auth]
policy_guard_names = ["MyAppGuarded", "PolicyExtractor"]
Matched as last-segment + case-insensitive starts_with (so a single entry "Guarded" covers Guarded, GuardedData, GuardedRoute). Distinct from login_guard_names and admin_guard_names.
#Recognised actor names
Recognised by default: user.id, user.user_id, user.uid, session.user_id, current_user.id, plus typed extractor parameters with CurrentUser, SessionUser, AuthUser, Extension<...> shapes. To add a custom binding pattern, file an issue or add a fixture; the heuristic is in src/auth_analysis/checks.rs under extract_validation_target and friends.
#Suppress
Inline:
db.insert(widget_id, value)?; // nyx:ignore rs.auth.missing_ownership_check
Or filter by severity / confidence in CI:
nyx scan . --severity ">=MEDIUM" --min-confidence medium
#In the UI
Auth findings render alongside taint findings in the browser UI. The flow visualiser shows the sink call, the actor reference (when one was found), and any helper-summary path the engine traversed; the How to fix panel mirrors the rule's recommendation.

#Benchmark corpus
The Rust auth corpus at tests/benchmark/corpus/rust/auth/ covers the recognised authorization patterns, true-positive controls, typed-extractor guard injection, and the project-level web-framework gate (full-Cargo.toml fixtures under safe_non_web_rust_project/ and unsafe_actix_web_project_no_check/). Per-row metrics live under the Rust auth row in tests/benchmark/RESULTS.md.