v7.3.1
Per-route checks now resolve through unbounded mount and router nesting (v7.3.1)
- Security — Fixes GHSA-f2vm-w8gq-h378 (CWE-287, per-route authentication bypass, CVSS 7.4).
SecurityMiddleware._match_route()capped its recursive walk of the route tree at a hardcoded depth of 8 and silently gave up beyond it, so an endpoint mounted 9 or moreMountlevels deep never resolved,request.state.guard_route_idwas never set, and guard-core's route-config resolver returnedNone— which every per-route check reads as "no policy to enforce". An endpoint protected by@require_authwas served to any unauthenticated client, and by the same path so were@rate_limit,@require_headers,@require_referrer,@custom_validation,@time_window, per-route IP restrictions, and request size / content-type limits. The arbitrary depth cap is replaced by cycle detection over the descent chain — the condition it was approximating — so nesting is now unbounded while a self-nesting router still terminates. - Security — The same bypass reached a second, more common topology: routers nested two or more levels deep via
include_router, previously recorded in 7.2.2 as "deeply-nested prefixed routers remain a known limitation". FastAPI's_IncludedRouterkeeps its sub-routes un-prefixed and applies the combined include prefix only ineffective_candidates(), so descending intooriginal_router.routescompared a request path like/r0/r1/secretagainst a route registered as/r1/secretand matched nothing. Resolution now descends througheffective_candidates()where available — falling back to the previous attribute walk on FastAPI versions without it — and unwraps the_EffectiveRouteContextproxies it yields. Per-route decorators consequently fire at any include depth, and also onMounts and plain StarletteRoutes registered on an included router, whichinclude_router(router, prefix=...)overrouter.mount(...)had likewise left unresolved. - Added —
SecurityMiddlewarenow reports a failed route match by settingrequest.state.guard_route_unresolved, which is the fail-open half of the advisory:get_route_config()returnsNoneboth when a route carries no per-route config and when resolution failed, and every per-route check collapses those two states into "nothing to enforce". Skipping checks on an undecorated route is correct and unchanged; skipping them because resolution failed is not. With guard-core >= 3.7.0 andSecurityConfig.route_resolution_strict=True, an unresolved request is logged, emits aroute_unresolvedevent, and is blocked with500instead of running the pipeline with no per-route config. It defaults toFalsebecause a failed match is indistinguishable from a request the app does not route, so enabling it also turns unserved paths into500s rather than404s. Older guard-core ignores the new state attribute. - Performance — Traversal is bounded by marking each route and sub-route collection as visited per request scope rather than by a fixed depth, so a graph reaching the same collection along many paths is walked once instead of exponentially often. An app with two overlapping sibling mounts per level resolves a non-matching 24-level path in 0.2 ms, against 99 s for a depth-capped walk without the marking. The one graph shape that still recurses without bound manufactures fresh sub-route objects on every access, which is not constructible through public FastAPI or Starlette APIs; it raises
RecursionErrorand surfaces as500rather than as a silently unchecked route. - Fixed —
SecurityConfig.global_behavior_rulesnever ran._process_response()passedprocess_behavioral_rulesto guard-core's response factory but neverprocess_global_behavioral_rules, and the factory only evaluates global rules when that callback is supplied, so the branch was unreachable on every request. A rule such asreturn_patternwiththreshold=2andaction="ban"served six matching requests without ever banning. The callback is now wired toBehavioralProcessor.process_global_return_rules. Global rules additionally need guard-core >= 3.7.0, which fixes the companion defect that left the behavioural tracker unresolved on decorator-only setups and madeusage_monitorandreturn_monitorinert too. - Tests — Route resolution and end-to-end
@require_authenforcement are now covered at 12 nestedMountlevels and at 12 nested prefixed routers, plus aMountinside a prefixed included router, alongside the existing self-nesting-router termination test. The global-rule callback is asserted at the call site; the end-to-end banning behaviour it enables is covered in guard-core.
What's Changed
- build(deps): bump actions/setup-python from 6 to 7 by @dependabot[bot] in #101
- build(deps): bump actions/labeler from 6 to 7 by @dependabot[bot] in #102
- ci: test the pull request's own code by @rennf93 in #103
- ci: run CI when workflows change by @rennf93 in #104
- fix(middleware): resolve routes through unbounded mount and router nesting (v7.3.1) by @rennf93 in #105
Full Changelog: 7.3.0...7.3.1