mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-12 19:55:14 +02:00
Performance and precision pass (#64)
This commit is contained in:
parent
c7c5e0f3a1
commit
fb698d2c27
97 changed files with 9932 additions and 517 deletions
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
@ -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
|
||||
|
|
@ -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 = {}
|
||||
|
|
@ -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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue