fastapi-guard 7.2.2__tar.gz → 7.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,8 @@ version_selector: true
2
2
  title_switch: true
3
3
  versions_file: docs/versions/versions.json
4
4
  versions:
5
+ - 7.3.1
6
+ - 7.3.0
5
7
  - 7.2.2
6
8
  - 7.2.1
7
9
  - 7.2.0
@@ -48,4 +50,4 @@ versions:
48
50
  - 0.3.2
49
51
  - latest
50
52
  aliases:
51
- latest: 7.2.2
53
+ latest: 7.3.1
@@ -3,6 +3,34 @@ Release Notes
3
3
 
4
4
  ___
5
5
 
6
+ v7.3.1 (2026-07-29)
7
+ -------------------
8
+
9
+ Per-route checks now resolve through unbounded mount and router nesting (v7.3.1)
10
+ --------------------------------------------------------------------------------
11
+
12
+ - **Security** — Fixes [GHSA-f2vm-w8gq-h378](https://github.com/rennf93/fastapi-guard/security/advisories/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 more `Mount` levels deep never resolved, `request.state.guard_route_id` was never set, and guard-core's route-config resolver returned `None` — which every per-route check reads as "no policy to enforce". An endpoint protected by `@require_auth` was 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.
13
+ - **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 `_IncludedRouter` keeps its sub-routes un-prefixed and applies the combined include prefix only in `effective_candidates()`, so descending into `original_router.routes` compared a request path like `/r0/r1/secret` against a route registered as `/r1/secret` and matched nothing. Resolution now descends through `effective_candidates()` where available — falling back to the previous attribute walk on FastAPI versions without it — and unwraps the `_EffectiveRouteContext` proxies it yields. Per-route decorators consequently fire at any include depth, and also on `Mount`s and plain Starlette `Route`s registered on an included router, which `include_router(router, prefix=...)` over `router.mount(...)` had likewise left unresolved.
14
+ - **Added** — `SecurityMiddleware` now reports a failed route match by setting `request.state.guard_route_unresolved`, which is the fail-open half of the advisory: `get_route_config()` returns `None` both 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 and `SecurityConfig.route_resolution_strict=True`, an unresolved request is logged, emits a `route_unresolved` event, and is blocked with `500` instead of running the pipeline with no per-route config. It defaults to `False` because a failed match is indistinguishable from a request the app does not route, so enabling it also turns unserved paths into `500`s rather than `404`s. Older guard-core ignores the new state attribute.
15
+ - **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 `RecursionError` and surfaces as `500` rather than as a silently unchecked route.
16
+ - **Fixed** — `SecurityConfig.global_behavior_rules` never ran. `_process_response()` passed `process_behavioral_rules` to guard-core's response factory but never `process_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 as `return_pattern` with `threshold=2` and `action="ban"` served six matching requests without ever banning. The callback is now wired to `BehavioralProcessor.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 made `usage_monitor` and `return_monitor` inert too.
17
+ - **Tests** — Route resolution and end-to-end `@require_auth` enforcement are now covered at 12 nested `Mount` levels and at 12 nested prefixed routers, plus a `Mount` inside 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, since it cannot fire until 3.7.0 ships.
18
+
19
+ ___
20
+
21
+ v7.3.0 (2026-07-15)
22
+ -------------------
23
+
24
+ Security pipeline assembly delegated to guard-core's check factory (v7.3.0)
25
+ ---------------------------------------------------------------------------------
26
+
27
+ - **Changed** — The security pipeline is now assembled by guard-core's `build_default_pipeline()`. New guard-core checks are picked up automatically; the middleware no longer hand-lists check classes.
28
+ - **Compatibility (behavior change from guard-core 3.5.0)** — Global IP whitelist/blacklist and country rules now apply on routes carrying per-route decorator config; previously any decorated route silently bypassed every global IP and country rule. A client outside a configured global `whitelist` now receives `403` on decorated routes that previously served it. A route-level `@ip_whitelist` now grants access only, not trust: the matched request is still rate-limited, user-agent-filtered, cloud-provider-checked, attack-scanned, and country-checked, while global `whitelist` membership still confers full trust. The IP and country aspects are evaluated independently — a route `@ip_whitelist` match does not bypass country rules; only a route `@whitelist_countries` match overrides the global country gate. See the guard-core 3.5.0 changelog for the full description and migration guidance.
29
+ - **Tests** — The decorator test suite now exercises the rate-limiting, user-agent, and suspicious-activity decorators through the enforced security pipeline using a non-whitelisted client, with explicit coverage for the new decorated-route `403` behavior and for a route `@ip_whitelist` client still being rate-limited. Previously these tests sent requests from a globally-whitelisted address, so the security pipeline short-circuited and the decorator logic was never exercised.
30
+ - **Requires** — `guard-core>=3.5.0` (declared as unconstrained `guard-core` in pyproject; documented here for upgrade guidance). The pipeline factory (`build_default_pipeline`) and the decorated-route IP/country enforcement this release relies on ship in guard-core 3.5.0; installing against an older guard-core fails at middleware initialization with an `ImportError`.
31
+
32
+ ___
33
+
6
34
  v7.2.2 (2026-07-01)
7
35
  -------------------
8
36
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi_guard
3
- Version: 7.2.2
3
+ Version: 7.3.1
4
4
  Summary: Production-ready security middleware for FastAPI — IP filtering, rate limiting, penetration detection, and 20+ per-route security decorators.
5
5
  Project-URL: Homepage, https://guard-core.com
6
6
  Project-URL: Documentation, https://rennf93.github.io/fastapi-guard/latest/
@@ -271,6 +271,21 @@ Published on crates.io. **🚧 Placeholder crates — implementation in progress
271
271
  | [rocket-guard-rs](https://github.com/rennf93/rocket-guard-rs) | Rocket adapter | [![crates.io](https://img.shields.io/crates/v/rocket-guard-rs)](https://crates.io/crates/rocket-guard-rs) |
272
272
  | [tower-guard-rs](https://github.com/rennf93/tower-guard-rs) | Tower adapter | [![crates.io](https://img.shields.io/crates/v/tower-guard-rs)](https://crates.io/crates/tower-guard-rs) |
273
273
 
274
+ ### AI Coding Agents
275
+
276
+ | Package | Role | PyPI |
277
+ |---|---|---|
278
+ | [guard-core-mcp](https://github.com/rennf93/guard-core-mcp) | MCP server — config validation, docs search, detection sandbox | [![PyPI](https://img.shields.io/pypi/v/guard-core-mcp)](https://pypi.org/project/guard-core-mcp/) |
279
+
280
+ An MCP server that answers questions about FastAPI Guard from the version **installed in your project**, rather than from a model's memory of it. It validates a config against the real `SecurityConfig` model — catching silently-ignored typos like `redis_failopen` — looks up any field's type, default and description, searches the bundled docs, and runs a payload through the real detection engine to show whether it would be blocked and by which pattern.
281
+
282
+ ```bash
283
+ uv add --dev guard-core-mcp
284
+ claude mcp add guard-core -- uv run guard-core-mcp
285
+ ```
286
+
287
+ Install it into the same environment as FastAPI Guard — it introspects what is actually installed there, so an isolated run (`uvx`) has nothing to read.
288
+
274
289
  ---
275
290
 
276
291
  ## Documentation
@@ -206,6 +206,21 @@ Published on crates.io. **🚧 Placeholder crates — implementation in progress
206
206
  | [rocket-guard-rs](https://github.com/rennf93/rocket-guard-rs) | Rocket adapter | [![crates.io](https://img.shields.io/crates/v/rocket-guard-rs)](https://crates.io/crates/rocket-guard-rs) |
207
207
  | [tower-guard-rs](https://github.com/rennf93/tower-guard-rs) | Tower adapter | [![crates.io](https://img.shields.io/crates/v/tower-guard-rs)](https://crates.io/crates/tower-guard-rs) |
208
208
 
209
+ ### AI Coding Agents
210
+
211
+ | Package | Role | PyPI |
212
+ |---|---|---|
213
+ | [guard-core-mcp](https://github.com/rennf93/guard-core-mcp) | MCP server — config validation, docs search, detection sandbox | [![PyPI](https://img.shields.io/pypi/v/guard-core-mcp)](https://pypi.org/project/guard-core-mcp/) |
214
+
215
+ An MCP server that answers questions about FastAPI Guard from the version **installed in your project**, rather than from a model's memory of it. It validates a config against the real `SecurityConfig` model — catching silently-ignored typos like `redis_failopen` — looks up any field's type, default and description, searches the bundled docs, and runs a payload through the real detection engine to show whether it would be blocked and by which pattern.
216
+
217
+ ```bash
218
+ uv add --dev guard-core-mcp
219
+ claude mcp add guard-core -- uv run guard-core-mcp
220
+ ```
221
+
222
+ Install it into the same environment as FastAPI Guard — it introspects what is actually installed there, so an isolated run (`uvx`) has nothing to read.
223
+
209
224
  ---
210
225
 
211
226
  ## Documentation
@@ -242,51 +242,13 @@ class SecurityMiddleware(BaseHTTPMiddleware):
242
242
  return {"enabled": True, "degraded": self.agent_degraded, **handler_stats}
243
243
 
244
244
  def _build_security_pipeline(self) -> None:
245
- from guard_core.core.checks import (
246
- AuthenticationCheck,
247
- CloudIpRefreshCheck,
248
- CloudProviderCheck,
249
- CustomRequestCheck,
250
- CustomValidatorsCheck,
251
- EmergencyModeCheck,
252
- HttpsEnforcementCheck,
253
- IpSecurityCheck,
254
- RateLimitCheck,
255
- ReferrerCheck,
256
- RequestLoggingCheck,
257
- RequestSizeContentCheck,
258
- RequiredHeadersCheck,
259
- RouteConfigCheck,
260
- SecurityCheckPipeline,
261
- SuspiciousActivityCheck,
262
- TimeWindowCheck,
263
- UserAgentCheck,
264
- )
245
+ from guard_core.core.checks import build_default_pipeline
265
246
 
266
- checks = [
267
- RouteConfigCheck(self),
268
- EmergencyModeCheck(self),
269
- HttpsEnforcementCheck(self),
270
- RequestLoggingCheck(self),
271
- RequestSizeContentCheck(self),
272
- RequiredHeadersCheck(self),
273
- AuthenticationCheck(self),
274
- ReferrerCheck(self),
275
- CustomValidatorsCheck(self),
276
- TimeWindowCheck(self),
277
- CloudIpRefreshCheck(self),
278
- IpSecurityCheck(self),
279
- CloudProviderCheck(self),
280
- UserAgentCheck(self),
281
- RateLimitCheck(self),
282
- SuspiciousActivityCheck(self),
283
- CustomRequestCheck(self),
284
- ]
285
-
286
- self.security_pipeline = SecurityCheckPipeline(checks)
247
+ pipeline = build_default_pipeline(self)
248
+ self.security_pipeline = pipeline
287
249
  self.logger.info(
288
- f"Security pipeline initialized with {len(checks)} checks: "
289
- f"{self.security_pipeline.get_check_names()}"
250
+ f"Security pipeline initialized with {len(pipeline)} "
251
+ f"checks: {pipeline.get_check_names()}"
290
252
  )
291
253
 
292
254
  def _configure_security_headers(self, config: SecurityConfig) -> None:
@@ -366,6 +328,9 @@ class SecurityMiddleware(BaseHTTPMiddleware):
366
328
  response_time,
367
329
  route_config,
368
330
  process_behavioral_rules=self.behavioral_processor.process_return_rules,
331
+ process_global_behavioral_rules=(
332
+ self.behavioral_processor.process_global_return_rules
333
+ ),
369
334
  )
370
335
  return unwrap_response(result)
371
336
 
@@ -377,30 +342,58 @@ class SecurityMiddleware(BaseHTTPMiddleware):
377
342
  routes = getattr(app, "routes", None)
378
343
  if not routes:
379
344
  return None
380
- return self._match_route(routes, request.scope, 0)
345
+ return self._match_route(routes, request.scope, set())
381
346
 
382
- def _match_route(self, routes: Any, scope: Any, depth: int) -> Any:
347
+ def _match_route(self, routes: Any, scope: Any, seen: set[Any]) -> Any:
383
348
  # This middleware is a BaseHTTPMiddleware, so it runs before the router
384
349
  # and scope["route"] is unset. Replicate Starlette's matching, descending
385
350
  # into sub-routers / mounts (FastAPI's include_router inserts a wrapper
386
351
  # route whose real routes are nested inside), so per-route decorator
387
352
  # config resolves for path-parameter and include_router routes, not only
388
353
  # routes flattened directly onto the app.
354
+ if not self._first_visit(seen, routes, scope):
355
+ return None
389
356
  for r in routes:
390
357
  matched, child = self._route_full_match(r, scope)
391
358
  if not matched:
392
359
  continue
393
- if hasattr(r, "endpoint"):
360
+ r = self._unwrap_candidate(r)
361
+ if getattr(r, "endpoint", None) is not None:
394
362
  return r
395
- if depth >= 8:
363
+ child_scope = {**scope, **child}
364
+ if not self._first_visit(seen, r, child_scope):
396
365
  continue
397
- found = self._match_route(
398
- self._sub_routes(r), {**scope, **child}, depth + 1
399
- )
366
+ sub_routes = self._sub_routes(r)
367
+ if not sub_routes:
368
+ return r
369
+ found = self._match_route(sub_routes, child_scope, seen)
400
370
  if found is not None:
401
371
  return found
402
372
  return None
403
373
 
374
+ def _first_visit(self, seen: set[Any], node: Any, scope: Any) -> bool:
375
+ # Replaces a fixed depth cap, which silently abandoned the walk and left
376
+ # deeply nested routes unresolved. Marking the route and its sub-route
377
+ # collection stops mount cycles and keeps a graph that reaches the same
378
+ # collection by many paths linear rather than exponential. Ids are safe as
379
+ # keys: every collection walked is owned by a live app, router or route
380
+ # held on the call stack, so none can be freed and recycled mid-walk.
381
+ key = (id(node), scope.get("path"), scope.get("root_path"))
382
+ if key in seen:
383
+ return False
384
+ seen.add(key)
385
+ return True
386
+
387
+ def _unwrap_candidate(self, route: Any) -> Any:
388
+ # FastAPI's include machinery matches through _EffectiveRouteContext proxies
389
+ # that hold the prefix-corrected route; that inner route is what carries the
390
+ # endpoint and the sub-routes, and its child scope is already applied here.
391
+ return (
392
+ getattr(route, "starlette_route", None)
393
+ or getattr(route, "original_route", None)
394
+ or route
395
+ )
396
+
404
397
  def _route_full_match(self, route: Any, scope: Any) -> tuple[bool, Any]:
405
398
  matches = getattr(route, "matches", None)
406
399
  if matches is None:
@@ -412,6 +405,12 @@ class SecurityMiddleware(BaseHTTPMiddleware):
412
405
  return match == Match.FULL, child
413
406
 
414
407
  def _sub_routes(self, route: Any) -> Any:
408
+ # FastAPI's _IncludedRouter keeps sub-routes un-prefixed and only applies the
409
+ # combined include prefix in effective_candidates(); descending into
410
+ # original_router.routes instead resolves nothing past one nesting level.
411
+ candidates = getattr(route, "effective_candidates", None)
412
+ if callable(candidates):
413
+ return candidates()
415
414
  included = getattr(route, "original_router", None)
416
415
  mounted = getattr(route, "app", None)
417
416
  return (
@@ -431,10 +430,14 @@ class SecurityMiddleware(BaseHTTPMiddleware):
431
430
  guard_request.state.guard_decorator = app_decorator
432
431
 
433
432
  route = self._resolve_route(request)
434
- if not route or not hasattr(route, "endpoint"):
433
+ if not route:
434
+ guard_request.state.guard_route_unresolved = True
435
+ return
436
+
437
+ ep = getattr(route, "endpoint", None)
438
+ if ep is None:
435
439
  return
436
440
 
437
- ep = route.endpoint
438
441
  if hasattr(ep, "_guard_route_id"):
439
442
  guard_request.state.guard_route_id = ep._guard_route_id
440
443
  if hasattr(ep, "__module__") and hasattr(ep, "__qualname__"):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fastapi_guard"
3
- version = "7.2.2"
3
+ version = "7.3.1"
4
4
  description = "Production-ready security middleware for FastAPI — IP filtering, rate limiting, penetration detection, and 20+ per-route security decorators."
5
5
  authors = [
6
6
  {name = "Renzo Franceschini", email = "rennf93@users.noreply.github.com"}
File without changes
File without changes
File without changes
File without changes