Repository navigation
Releases: Guard-Core/fastapi-guard
Release list
v8.1.0
v8.1.0 (2026-10-10)
The pure-ASGI release: the full screening pipeline without BaseHTTPMiddleware (v8.1.0)
Added
PureASGISecurityMiddleware: the full screening pipeline on raw ASGI (fastapi-guard #156). Runs initialization, CORS preflight, path exclusions, IP extraction, ban lists, rate limiting, user-agent blocking, penetration detection and behavioral usage rules withoutBaseHTTPMiddleware- no anyio task group wraps the downstream app, so client disconnects can never cancel in-flight database calls, and responses stream through unbuffered. Body reads go through a caching receive that replays to the application; security and CORS headers are injected directly onhttp.response.start; WebSocket and lifespan scopes pass through untouched. Built for frameworks that banBaseHTTPMiddlewareby policy (open-webui carries a written rationale in main.py) and for the RFC thread asking for exactly this shape.- Scope note: decorator-driven response rules (return patterns) and
custom_response_modifierneed a materializedResponseobject and stay on the classic middleware; both classes ship side by side and the classic one is unchanged.
v8.0.3
v8.0.2
v8.0.2 (2026-09-26)
Community fixes and exempt_ips lockstep: trailing-slash route config, block response Content-Type, live-smoke exempt_ips scenario; guard-core 4.1.0 tracking (v8.0.2)
- Fixed - Route decorator config now resolves through Starlette's trailing-slash redirect (#142): when the request path matches no route but toggling the trailing slash does, the middleware reads the config of the route Starlette would 307-redirect to, so a route override registered on
/pathnow governs a request to/path/(and vice versa) instead of silently falling through to the global defaults. Each router decides with its ownredirect_slashes(mounted apps for their paths, the including app for an included router's routes), the root path and directly matched requests are untouched. - Fixed - Block responses now declare
Content-Type: text/plain(#144): the Starlette response factory left Starlette's media type unset, so middleware-generated 403/429/400 pages went out without a content type and clients were left guessing. The factory now passesmedia_type="text/plain", matching the engine's reference response factory. - Tests - New live-smoke scenario
tests/live_smoke/scenarios/exempt_ips.py(#145) covering the guard-coreexempt_ipsskip-list end to end through the live stack: an exempt IP exceedingrate_limitkeeps getting 200 across the crossing while no rate-limit bucket is written for it at all (smoke:rate_limit:rate:<ip>stays absent in Redis, pinning that the exempt skip sits ahead of the limiter), a penetration-detection payload from the exempt IP still gets 400 (exemption is not immunity), the identical sequence withoutexempt_ipsthrottles at the crossing with 429 and leaves exactly the bucket the exempt run never wrote, and an IP that is both exempt and blacklisted gets 403 (blacklist beats exemption). The scenario merged red against PyPI guard-core (< 4.1.0) by design, the documented lockstep hold; with this release it runs green against the published 4.1.0 engine in CI. - Compatibility - The
guard-corefloor rises from>=4.0.0to>=4.1.0and this release tracks the guard-core 4.1.0 engine (the exempt_ips skip-list, the recon leading-separator gate, the raw-view recon scan, the cross-engine interop harness). Both fixed behaviors are adapter-side; no guard-core API fastapi-guard uses changed shape.
v8.0.1
CI hardening, live smoke suite, agent docs; guard-core 4.0.4 tracking (v8.0.1)
- CI - Added a DockSec container scan with SARIF upload to the pipeline (#138) and removed the Slack notification steps from all workflows.
- CI - New live smoke suite (
tests/live_smoke/): a Docker Compose stack (nginx, gunicorn, Redis) running the full example app is exercised against PRs, and a nightly upstream drift gate builds the suite against guard-core master so engine-side regressions surface before a release instead of after (#134). - Fixed (tests) -
test_cloud_ip_blocking_with_logging(tests/test_middleware/test_security_middleware.py) now awaits the scheduled background cloud-IP refresh before asserting instead of relying on a stale Redis handler left behind by another test to suspend the dispatch (#136). - Documentation - Added
AGENTS.md/CLAUDE.mdagent guidance and extended the fastapi-guard package skill. - Compatibility - No production code changed: the delta since 8.0.0 is CI, tests and docs only, hence the patch bump. The
guard-core>=4.0.0floor is unchanged and this release tracks the guard-core 4.0.4 engine.
v8.0.0
Lockstep with guard-core 4.0.0: log_activity assertions, timestamp serialisation, unban wiring, throttle docs, hsts None guard (v8.0.0)
- Compatibility (dependency floor, breaking for anyone pinned below 4.0.0) -
guard-corefloor moves from>=3.14.0to>=4.0.0inpyproject.toml, no upper bound. guard-core 4.0.0 carries breaking changes of its own (grammar-based secret redaction,require_headers()now enforces an exact value match for any configured value other than"required",excluded_detection_headersno longer fully silences a header) which do not require any change in this release's own code or tests, since fastapi-guard'srequire_headers()usage in its examples and tests only ever exercises the presence-only"required"value or sends a header value matching what is configured, and neither fastapi-guard's code nor its tests setexcluded_detection_headers. See the guard-core 4.0.0 changelog for the full engine-side list. - Requires (lockstep) - fastapi-guard 8.0.0 tracks guard-core 4.0.0. guard-core 4.0.0 is not yet published to PyPI as of this release commit; CI resolves
guard-core>=4.0.0fresh from PyPI (uv.lockis gitignored) and will fail until it is. This release is held locally, matching the pattern set by earlier lockstep releases, until guard-core 4.0.0 ships. - Tests -
test_cloud_ip_blocking_with_loggingandtest_passive_mode_penetration_detection(tests/test_middleware/test_security_middleware.py) pinned the fulllog_activity()call viaassert_any_call, which broke against guard-core >=3.16.0's addedon_block/sensitive_headers/sensitive_params/sensitive_body_fieldskeyword arguments. Both now check only the kwargs each test cares about as a subset of the actual call (assert_log_activity_called_with), tolerating any further keyword arguments guard-core adds. - Fixed (examples) -
http_exception_handlerandgeneral_exception_handler(examples/advanced_app/app/main.py) dumpedErrorResponsewithmodel_dump(), leavingtimestampas a rawdatetimethatJSONResponsecannot encode, turning every error response (for example/behavior/return-monitor/404) into an unhandled 500. Both now usemodel_dump(mode="json"). - Fixed (examples) -
/admin/unban-ip(examples/advanced_app/app/routes/admin.py) only logged the unban and never called guard-core's IP ban manager, so a banned IP stayed banned. The route now callsip_ban_manager.unban_ip(ip). - Documentation (examples) -
action="throttle"only logs the violation; guard-core never blocks or delays the request for it. Corrected the/behavior/suspicious-frequencyand/behavior/behavior-rulesroute descriptions and dropped the fabricated429response entries that promised otherwise, and corrected the matching lines indocs/tutorial/decorators/behavioral.mdanddocs/api/behavior-manager.md. - Documentation (examples) -
/behavior/usage-monitor'sresponsesentry named a"block"action that does not exist in guard-core (the route usesaction="log"and never returns 429); the entry is removed.alertwas documented as sending a notification; it is a critical-level log call, same as any other action's telemetry event. Corrected inexamples/advanced_app/app/routes/behavioral.pyanddocs/api/behavior-manager.md. - Fixed -
SecurityMiddleware._configure_security_headers(guard/middleware.py) calledheaders_config.get("hsts", {}).get("max_age"), which raisedAttributeErroron every request whensecurity_headers={"hsts": None, ...}was passed explicitly, since.get("hsts", {})returnsNone, not{}, for a key present with an explicitNonevalue. Now falls back to{}the same way guard-core's ownsecurity_headers_handlertreats a missinghstsblock. guard-core 4.0.0 separately fixes the same class of bug on its own singleton (a stalehsts/csp/corsconfiguration surviving a reconfigure), so this guard is defense in depth against both the old and the fixed guard-core behavior. - Security -
guard_websocketandmake_guard_websocket(guard/websocket.py) ran only the IP-ban and rate-limit checks on a WebSocket handshake; an attack riding in the handshake's headers, cookies, query string or path was never detected and never logged. The handshake is now scanned the same way an HTTP request is scanned, through guard-core's ownSuspiciousActivityCheck, honouringenable_penetration_detection,passive_mode,exclude_pathsand the whitelist and auto-ban rules already in place; a detected attack in active mode closes the connection beforeaccept()with the newWS_CLOSE_SUSPICIOUS_ACTIVITYclose reason (code1008, collected inWS_CLOSE_REASONS), logs and emits the same event andon_blockpayload the HTTP pipeline emits, and redacts sensitive header, query and cookie values exactly as the HTTP pipeline does; passive mode logs and emits without closing the connection.
What's Changed
- test: assert guard-core 3.15.0 redis_fail_open semantics by @rennf93 in #127
- ci: issue-first workflow and Discord contact points by @rennf93 in #125
- build(deps): bump actions/github-script from 7 to 9 by @dependabot[bot] in #128
- release(v8.0.0): lockstep with guard-core 4.0.0, example and middleware fixes by @rennf93 in #130
Full Changelog: 7.8.2...8.0.0
v7.8.2
Route-relative path resolution under root_path and mounts, Starlette and FastAPI floors (v7.8.2)
- Fixed -
StarletteGuardRequest.url_pathand the WebSocket adapter's_WebSocketGuardRequest.url_pathreturnedrequest.url.path/websocket.url.path, which under an ASGIroot_path(uvicorn --root-path, or a mounted sub-app) includes the mount prefix. guard-core matches that value againstSecurityConfig.exclude_pathsandendpoint_rate_limits(an exact dict-key match) and passes it tocustom_request_checkhooks, so a key such as/api/vault/loginnever matched a request actually served at/mounted/api/vault/loginand every path-keyed control silently stopped applying under a mount. Both adapters now return Starlette's ownget_route_path(scope), the same route-relative path Starlette's router already matches routes against. - Compatibility - If
exclude_pathsorendpoint_rate_limitskeys currently include yourroot_pathmount prefix (a workaround for the bug above), remove the prefix after upgrading: those keys now match the route-relative path and a prefixed key silently stops matching. The same applies tocustom_request_checkhooks that branch onurl_path. - Compatibility -
starlette>=0.35.0andfastapi>=0.109.0are now declared floors (no ceilings). Route-relative path resolution usesstarlette.routing.get_route_path, which Starlette added in 0.35.0; 0.109.0 is the first FastAPI release that requires that Starlette. Older pairs fail atimport guardwith anImportError. - Changed - Detection input and the reason text of the
Suspicious activity detectedlog line now use the route-relative path, while theGET <url>part of the same line still shows the full URL including the mount prefix (url_fullis unchanged), so the two differ under a mount by design.
What's Changed
Full Changelog: 7.8.1...7.8.2
v7.8.1
Websocket guard follow-ups: whitelist parity, explicit-config factory, close-code contract, state.client_ip (v7.8.1)
- Added -
make_guard_websocket(config: SecurityConfig, redis_handler: RedisManager | None = None)returns aDepends-ready WebSocket dependency bound to an explicitSecurityConfig, for a route on an app that never callsapp.add_middleware(SecurityMiddleware, config=...);guard_websocketis unchanged and still resolves its config from the registered middleware, raisingRuntimeErrorwithout one. Both entry points share one private check coroutine, so every rule applies to both. - Added -
guardexports module-level(code, reason)constants for the five WebSocket close outcomesguard_websocket/make_guard_websocketcan emit (WS_CLOSE_IP_BANNED,WS_CLOSE_IP_NOT_ALLOWED,WS_CLOSE_RATE_LIMIT_EXCEEDED,WS_CLOSE_CLIENT_ADDRESS_UNKNOWN,WS_CLOSE_SECURITY_CHECK_FAILED, collected inWS_CLOSE_REASONS), documented as a stable contract indocs/tutorial/websockets.mdthat a close handler can key logging on. - Added -
websocket.state.client_ipis now set to the resolved client identity (including"unknown") immediately after resolution, before any check runs, so a route handler can read it afteraccept(). - Fixed -
guard_websocketandmake_guard_websocketnow skip the rate-limit check for a client that passes a configured globalwhitelist, mirroring guard-core's HTTP rule (RateLimitCheck.checkreturns early once a request is resolved as whitelisted); previously a whitelisted WebSocket client could still be rate-limited. - Fixed - The websocket rate-limit check no longer constructs a
RedisManageritself.RedisManageris a process-wide singleton; constructing one with a differentSecurityConfigrebinds its connection for every other caller in the process, soguard_websocketpreviously risked overwriting the HTTP pipeline's own Redis manager, andmake_guard_websocketon an app withoutSecurityMiddlewarecould construct an uninitialized manager and raiseGuardRedisErroron every connect.guard_websocketnow resolves the middleware's already-initialized manager instead of constructing one;make_guard_websockettakes an optionalredis_handlerparameter and falls back to the in-memory store (logging one warning at dependency creation) whenenable_redis=Trueand none is given.
What's Changed
Full Changelog: 7.8.0...7.8.1
v7.8.0
Lockstep release tracking guard-core 3.14.0: missing-client rejection, forwarded-header joining, clean Redis-down responses, and WebSocket protection (v7.8.0)
Special thanks to @RomanPszonka for the reports and advisories behind this release and the support throughout. Thank you for making the Guard Core ecosystem better!
- Compatibility (dependency floor, breaking for anyone pinned below 3.14.0) -
guard-corechanges from an unconstrained dependency toguard-core>=3.14.0inpyproject.toml.pip install --upgrade fastapi-guardalone could previously leave an older guard-core in place; against guard-core 3.12.0 that is anImportErroratimport guardtime (check_ip_access/check_rate_limit_by_ip/is_ip_allowed, added in 3.13.0). No upper bound; the floor moves with each lockstep release (fg #120). - Security (GHSA-8xvm-856x-7hwp) -
StarletteGuardRequest.headersnow joins repeated field lines (RFC 7230 5.2) before guard-core resolves the client fromX-Forwarded-For; previously only the first line was visible, letting a client's own forged line hide the real proxy-appended one and rotate past rate limiting. - Compatibility (GHSA-634g-4wr8-xwxv) - A request with no client address is now rejected by default (
fail_secure=True) instead of skipping the entire security pipeline; this is guard-core 3.14.0's own fix, fastapi-guard's test suite and docs are brought in line with it here. New"unix"trusted_proxiestoken for Unix-socket and serverless deployments. - Fixed (#76) -
SecurityMiddlewareno longer crashes with an unhandled 500 (or fails app startup) when Redis is unreachable at initialization.dispatchnow catchesGuardRedisErrorand returns a clean 503 withRetry-After: 5, retrying on the next request; the lifespan warmer logs and continues instead of crashing boot. - Security (GHSA-63qv-gh36-52qf) - New
guard_websocketdependency (Depends(guard_websocket)) for@app.websocketroutes:SecurityMiddlewarenever runs for WebSocket scopes (BaseHTTPMiddlewareskips non-HTTP), so a banned or blacklisted IP, or one over its rate limit, previously reachedaccept()unchecked. The example app's "WebSocket protection" claim is now true. A Redis failure during the handshake now followsredis_fail_openandfail_securethe same way the HTTP pipeline does, instead of leaking an unhandledGuardRedisError; a fail-secure refusal closes the connection with code 1013. - Added (guard-core #81 item 3) -
add_status_routenow acceptsdependencies=[...](FastAPI apps only), so the opt-in/_guard/statusroute can be gated behind an existing auth dependency instead of relying solely on proxy-level restriction. - Compatibility (lockstep) - fastapi-guard 7.8.0 tracks guard-core 3.14.0. guard-core 3.14.0 is not yet published to PyPI as of this release commit; CI resolves
guard-core>=3.14.0fresh from PyPI (uv.lockis gitignored) and will fail until it is. This commit is held locally, matching the pattern set by earlier lockstep releases, until guard-core 3.14.0 ships. See guard-core 3.14.0's release notes for the full engine-side list: bounded in-memory rate-limit and behavior-tracker stores, float anomaly statistics, a per-request scan value cap, the AzureCloud tag fix, GeoIP last-known-good on refresh failure, telemetry secret redaction, ban-address canonicalization, and new SSRF detection forms.
What's Changed
Full Changelog: 7.7.0...7.8.0
v7.7.0
Lockstep release tracking guard-core 3.13.0: detection hardening and the breaking auth-verifier requirement (v7.7.0)
Special thanks to @hsnyus-09 and @iam-niranjan for your GHSA reports and the support throughout. Thank you for making the Guard Core ecosystem and FastAPI Guard better!
- Security (auth-verifier, breaking) -
require_auth(type="bearer", verifier=None)andapi_key_auth(header_name=..., verifier=None)now require a resolvable verifier, supplied per route viaverifier=or globally viaSecurityConfig.auth_verifier; without one the request is rejected with 401 fail-closed. Previously a bare Bearer/Basic prefix or any API-key header value was accepted without validation. The old presence-only behavior is now the separate decoratorrequire_authorization_header(scheme="bearer"), documented as NOT authentication and mutually exclusive with the two auth decorators. The verifier contract isverifier(request, credential) -> Principal | None(sync or async in ASGI, sync-only in WSGI), and the authenticated principal lands onrequest.state.auth_principal. See GHSA-x96c-fcg2-x2f9, CWE-287. - Compatibility (lockstep) - fastapi-guard 7.7.0 tracks guard-core 3.13.0; guard-core remains an unconstrained dependency in
pyproject.tomlper project policy so fresh installs already resolve 3.13.0; this release is what makes that pairing whole. See guard-core 3.13.0's release notes (its CHANGELOG.md 3.13.0 section / release notes) for the full list, which covers the ReDoS validation backstop, the scan-window mechanism, a new deserialization category (CWE-502), all-category ingestion-bypass closures, and detection widenings. - Adapter - fastapi-guard itself adds no new security logic: it re-exports guard-core's surface unchanged, and the auth methods ride on the already-re-exported
SecurityDecorator, so there is no adapter-side code change beyond the version and dependency pairing.
Full Changelog: 7.6.0...7.7.0
v7.6.0
Bounded body reading: chunked requests are scanned again and response body rules work for the first time (v7.6.0)
Special thanks to @arpitjain099 for reporting GHSA-c2r5-9jw9-m8q5 and https://github.com/rennf93/fastapi-guard/security/advisories/GHSA-xv6g-49vj-7w9c ! Thank you for your reports, your clarity and cooperation. And for making fastapi-guard and guard-core more secure!
- Added -
StarletteGuardRequestimplements guard-core 3.12.0'sBoundedBodyReadercapability. A request without a usableContent-Length, which is everyTransfer-Encoding: chunkedrequest, previously reached penetration detection as an unreadable body and was waved through unscanned; with guard-core older than 3.12.0 the same request instead triggered the unbounded buffering that GHSA-c2r5-9jw9-m8q5 closed.read_body_prefixaggregates at mostmax_bytes(each chunk is sliced before it is collected, locked in by an allocation-tracing regression test); an oversized single ASGI message the server has already delivered is retained once, by reference, for replay, never duplicated. The replaying receive is installed before the first pull, so a timed-out read cannot lose captured data, and the downstream endpoint observes the byte-identical full body provided the middleware is the first reader of the request stream, which it is in every supported configuration (Starlette's request body is single-consumer; an outer ASGI middleware that partially consumes the stream first forfeits that prefix for everyone downstream, adapter or not). Both properties are locked in by end-to-end tests through a real FastAPI app: an attack in a chunked body is blocked with400 Suspicious activity detected, and a benign chunked body arrives at the endpoint whole. - Fixed - Body-based
return_patternrules have never worked in any shipped fastapi-guard; they work now, as an opt-in. Starlette'sBaseHTTPMiddleware.call_next()always returns a_StreamingResponse, whose constructor never sets.body; the adapter'sbodyproperty raisedAttributeError, guard-core's oldhasattr(response, "body")gate swallowed the exception, and every body rule silently evaluated to no-match whilestatus:rules kept working. Anyone who wrote a body rule got silence and a passing config, with no way to discover the rule was dead.StarletteGuardResponsenow implementsBoundedResponseBodyReaderas a tee: it reads the first streamed chunk (a non-streaming response is scanned whole) up tobehavior_max_response_body_inspect_bytes, never pulls a second chunk from an indefinite stream such as SSE, and replays everything captured so the client receives the response unchanged. End-to-end tests prove a matching body rule now escalates to a ban and a streaming response arrives intact while being scanned. - Behaviour (three cases, stated exactly) -
behavior_scan_response_bodydefaults to False, so nothing changes for anyone who does not opt in. A body-basedreturn_patternrule with the flag off now raisesValueErrorat config or decorator construction naming the offending pattern, instead of shipping a rule that can never match. Only a deliberate opt-in on an adapter without the protocol (any fastapi-guard before this release) skips the rule, with guard-core's throttled could-not-evaluate warning naming it. - Compatibility (lockstep, do not soften) - Upgrading guard-core to 3.12.0 without upgrading fastapi-guard to 7.6.0 silently drops every
return_patternbody rule even withbehavior_scan_response_body=Trueexplicitly set. Upgrading fastapi-guard first is safe in either order: with older guard-core the new adapter methods are simply never called. guard-core stays an unconstrained dependency inpyproject.tomlper this project's policy, so fresh installs already resolve 3.12.0; this release is what makes that pairing whole. - Compatibility (guard-core 3.12.0 breaking changes surfaced here) - Nine
SecurityConfigcollection fields are now immutable (whitelist,blacklist,trusted_proxiesas tuples;enabled_detection_categories,muted_event_types,muted_metric_types,muted_check_logs,block_cloud_providersas frozensets;threat_ban_configas a read-only mapping), andglobal_behavior_rulesis a tuple: in-place mutation raises, so reassign a whole new value instead; construction still accepts plain lists and sets.block_cloud_providersnow raisesValueErroron an unrecognized provider name instead of silently dropping it. fastapi-guard's own code, tests and examples were audited for both: nothing mutated these fields in place, and every reconfiguration already reassigns. - Fixed - Both example apps crashed at import under guard-core 3.12.0, and their
pattern="404"rules had never fired.examples/simple_appandexamples/advanced_appusedreturn_monitor(pattern="404")meaning HTTP status 404; under 3.12.0 that raises at decoration time, and as written the rule was also dead, a body substring match against a body that was never readable. Both now usestatus:404, which is what they always meant. The behavioral decorator tests opt intobehavior_scan_response_body=True, which makes them the first end-to-end exercise of body matching this repository has ever run. - Internal - The geo_ip_handler inert-config warning test asserted a warning guard-core 3.12.0 removed. The warning was undecidable at the
SecurityConfiglayer (route-level country lists, geo rate limits and dynamic rules all consume the handler without the global lists being set), so 3.12.0 removed it. The test file now asserts the warning's absence at both construction and middleware creation, renamed totest_geo_ip_handler_no_inert_warning.pyto describe the behavior it locks in. - Documentation - Field types updated for the immutable collections, the
exclude_pathsdescription corrected (detection and behavioral tracking are skipped on excluded paths; IP bans, blacklists, blocked countries, blocked cloud providers and rate limits are still enforced there), and every behavioral-rules example annotated with the flag and version requirement so no documented snippet raises under guard-core 3.12.0.
What's Changed
Full Changelog: 7.5.1...7.6.0