Performance and precision pass (#64)

This commit is contained in:
Eli Peter 2026-05-04 19:58:04 -04:00 committed by GitHub
parent c7c5e0f3a1
commit fb698d2c27
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
97 changed files with 9932 additions and 517 deletions

View file

@ -0,0 +1,44 @@
"""Recall guard for the Phase 1 caller-scope IPA fix.
Same shape as `safe_caller_scope_helper_under_authorized_route.py`, but
the router carries no route-level auth dep (`router = APIRouter()`).
The helper's `session.add` is reached from a route handler with no
authorization, so the engine MUST still fire
`missing_ownership_check` (and `token_override_without_validation`)
on the helper's sink.
Triggers `apply_caller_scope_propagation`'s soundness rule: a helper's
caller list must contain at least one caller with route-level non-Login
auth checks. When no caller is authorized, no propagation happens and
the helper's sinks fire as expected.
"""
from typing import Annotated
from uuid import UUID
from fastapi import APIRouter, Body
# Bare router — no Security dep at the boundary.
ti_id_router = APIRouter()
def _create_state_update(
*,
task_instance_id: UUID,
payload: dict,
session,
) -> None:
if payload.get("kind") == "reschedule":
session.add({"id": task_instance_id, "data": payload})
@ti_id_router.patch("/{task_instance_id}/state")
def ti_update_state(
task_instance_id: UUID,
payload: Annotated[dict, Body()],
session,
) -> None:
_create_state_update(
task_instance_id=task_instance_id,
payload=payload,
session=session,
)

View file

@ -1,19 +1,26 @@
"""
Vulnerable counterpart to safe_fastapi_route_dependencies_auth.py: same
shape but with NO `dependencies=[Depends(...)]` keyword arg on the route
decorator. The FastAPI ownership-check rule must still fire the
recognizer must not blanket-suppress every FastAPI route, only those
with an actual dependency-injected auth check.
FastAPI route shape but with NO `dependencies=[Depends(...)]` keyword
arg on the route decorator. The ownership-check rule must still fire
the dependency-injection recogniser must not blanket-suppress every
FastAPI route, only those with an actual dependency-injected auth
check.
Sink uses a qualified Django-style ORM call so the post-fix
classifier still recognises it (`receiver_is_simple_chain` requires a
non-chained receiver dot).
"""
from fastapi import FastAPI
router = FastAPI()
class Connection:
objects = None
@router.delete("/{connection_id}")
def delete_connection(connection_id: str, session):
def delete_connection(connection_id: str):
"""No auth — must still fire missing_ownership_check."""
connection = session.scalar(select(Connection).filter_by(conn_id=connection_id))
if connection is None:
raise HTTPException(404, "not found")
session.delete(connection)
Connection.objects.filter(id=connection_id).delete()
return {"ok": True}

View file

@ -0,0 +1,27 @@
"""SQLAlchemy variant of vuln_fastapi_route_no_dependencies.py: same FastAPI
route shape with NO `dependencies=[Depends(...)]` keyword arg, but the sink
is a real-world airflow-style SQLAlchemy queryset chain
`session.scalar(select(C).filter_by(conn_id=user_input))`.
Pre-fix the chain reduced to bare `["filter_by"]` and was suppressed by
`receiver_is_simple_chain`, blocking recall on this real-repo airflow shape.
The member_chain Python `function`-field traversal + `db_query_builder_roots`
extension restores recall.
Recall guard: ownership-check rule must fire on the chained query the
caller has no auth check.
"""
from fastapi import FastAPI
from sqlalchemy import select
router = FastAPI()
class Connection:
pass
@router.delete("/{connection_id}")
def delete_connection(connection_id: str, session):
"""No auth — must fire missing_ownership_check on the chained query."""
return session.scalar(select(Connection).filter_by(conn_id=connection_id))

View file

@ -0,0 +1,38 @@
"""Recall counterpart to safe_fastapi_route_security_scopes.py.
Precision guard for the Security-without-scopes path: a bare
`Security(callable)` with no `scopes=[...]` kwarg, or with an empty
`scopes=[]`, is NOT promoted from LoginGuard to AuthorizationCheck
the OAuth2 scope semantic only fires when scopes is non-empty. Without
scope enforcement the wrapper is functionally equivalent to
`Depends(callable)` plus a bare login check, so `missing_ownership_check`
must still fire on a downstream id-targeted ORM filter.
Recall guard: ownership-check rule must fire Security with no scopes
is conservative (treated as login-only), so the route is not promoted
to authorized.
"""
from fastapi import FastAPI, Security
def require_auth():
pass
router = FastAPI()
class TaskInstance:
pass
@router.patch(
"/{task_instance_id}/run",
dependencies=[Security(require_auth, scopes=[])],
)
def ti_run(task_instance_id: str, session):
return session.scalar(select(TaskInstance).filter_by(id=task_instance_id))
def select(_):
pass

View file

@ -0,0 +1,46 @@
"""Recall guard for the router-level Security-prop fix. When a router
is declared with NO `dependencies=` kwarg (`router = APIRouter(...)`),
attached routes that don't supply inline deps are genuinely
unauthorized the engine must still flag id-targeted writes as
`missing_ownership_check`. Without the gate the router-level extractor
would over-fire by treating every router as auth-providing.
Distilled from airflow
`task_instances.py:1036-1082` where `router = VersionedAPIRouter()`
(bare, no deps) attaches `@router.get("/states", ...)` the route is
auth-attached only via the cross-file `include_router` chain in
`routes/__init__.py`, which is a separate gap (see deep_engine_fixes.md).
For the per-file case where the router has no router-level deps
declared, the route is correctly an un-guarded ownership-check FN.
"""
from cadwyn import VersionedAPIRouter
# Bare router — no router-level dependencies declared.
router = VersionedAPIRouter()
class TaskInstance:
pass
@router.get("/states/{run_id}/{task_id}")
def get_task_instance_states(run_id: str, task_id: str, session):
rows = session.scalars(
select(TaskInstance)
.where(TaskInstance.run_id == run_id)
.where(TaskInstance.task_id == task_id)
).all()
[
run_id_task_state_map[task.run_id].update(
{task.task_id: task.state}
)
for task in rows
]
def select(_):
pass
run_id_task_state_map = {}

View file

@ -0,0 +1,28 @@
# py-auth-realrepo-XXX (vuln pair): same bare-`set()` / `dict()` /
# `defaultdict()` local collection shape as
# safe_local_set_update_no_orm.py, but the helper *also* runs an
# id-targeted ORM query whose filter argument is a user-supplied id
# (`team_id` in the function signature, no caller-scope-entity
# exemption applies).
#
# Recall guard: the bare-callee constructor recogniser must only
# suppress the InMemoryLocal `.update` / `.add` calls — the
# id-targeted ORM `.filter(id=team_id)` must still fire
# `py.auth.missing_ownership_check`.
class Team:
pass
def get_team_with_history(request, team_id):
seen_ids = set()
audit = dict()
seen_ids.add(team_id)
audit["team"] = team_id
return Team.objects.filter(id=team_id).first()
def archive_team(request, team_id):
pending = set()
pending.add(team_id)
Team.objects.filter(id=team_id).delete()