Summary
SecurityMiddleware extends Starlette's BaseHTTPMiddleware
(guard/middleware.py:46). BaseHTTPMiddleware.__call__ opens with
if scope["type"] != "http": return await self.app(scope, receive, send), so a
websocket scope is handed straight to the router. Nothing in guard runs: no IP
allow/deny list, no ban check, no rate limit, no penetration detection, no
emergency lockdown. Route decorators on WebSocket handlers are inert too.
An address that is banned for HTTP can still open a socket and be served.
PoC
import asyncio
from fastapi import FastAPI, WebSocket
from starlette.testclient import TestClient
from guard.middleware import SecurityMiddleware
from guard_core.handlers.ipban_handler import ip_ban_manager
from guard_core.models import SecurityConfig
BANNED = ("203.0.113.66", 1234)
app = FastAPI()
app.add_middleware(SecurityMiddleware, config=SecurityConfig(enable_redis=False))
@app.get("/")
async def root():
return {"ok": True}
@app.websocket("/ws")
async def socket(sock: WebSocket):
await sock.accept()
await sock.send_text("data the ban was supposed to withhold")
await sock.close()
async def main():
await ip_ban_manager.ban_ip(BANNED[0], 3600)
with TestClient(app, client=BANNED) as client:
print("GET / ->", client.get("/").status_code)
with client.websocket_connect("/ws") as sock:
print("WS /ws ->", sock.receive_text())
asyncio.run(main())
On 7.7.0 / 3.13.0:
GET / -> 403
WS /ws -> data the ban was supposed to withhold
Impact
Any control the operator configured is absent on the WebSocket half of the
application. For an app that puts its streaming or realtime surface on
WebSockets - chat, notifications, live queries - that surface has no rate limit
and no ban enforcement at all, and the ban that the operator can see in the
admin state does not apply to it. Brute-forcing an authenticated socket, or
continuing an attack after being banned over HTTP, both work.
examples/simple_app advertises WebSocket protection, so an operator reading
the examples has positive reason to believe this is covered.
Suggested fix
Either implement __call__ at the ASGI level and run the identity, ban and rate
checks before the handshake is accepted, or say plainly in the README and the
example that the middleware is HTTP-only and ship a dependency
(Depends(guard_ws_check)) that a WebSocket route can use. The documentation
route is fine as a first step; the current state, where the example claims
coverage that does not exist, is the part that needs to change either way.
Fixed in 7.8.0
Fixed in fastapi-guard 7.8.0 (PyPI), merged in #121 (eb5f284). guard_websocket is a FastAPI dependency that runs the ban list, the IP and country allow lists and the rate limit against the resolved client before accept(), closing with 1008 on a policy violation; a client-less socket is rejected under fail_secure=True; Redis failures during the handshake follow redis_fail_open / fail_secure like the HTTP pipeline (close code 1013). BaseHTTPMiddleware never runs for websocket scopes, so protection is opt-in per route: Depends(guard_websocket). Documented in the websockets tutorial; the example app wires it on /ws.
Summary
SecurityMiddlewareextends Starlette'sBaseHTTPMiddleware(
guard/middleware.py:46).BaseHTTPMiddleware.__call__opens withif scope["type"] != "http": return await self.app(scope, receive, send), so awebsocketscope is handed straight to the router. Nothing in guard runs: no IPallow/deny list, no ban check, no rate limit, no penetration detection, no
emergency lockdown. Route decorators on WebSocket handlers are inert too.
An address that is banned for HTTP can still open a socket and be served.
PoC
On 7.7.0 / 3.13.0:
Impact
Any control the operator configured is absent on the WebSocket half of the
application. For an app that puts its streaming or realtime surface on
WebSockets - chat, notifications, live queries - that surface has no rate limit
and no ban enforcement at all, and the ban that the operator can see in the
admin state does not apply to it. Brute-forcing an authenticated socket, or
continuing an attack after being banned over HTTP, both work.
examples/simple_appadvertises WebSocket protection, so an operator readingthe examples has positive reason to believe this is covered.
Suggested fix
Either implement
__call__at the ASGI level and run the identity, ban and ratechecks before the handshake is accepted, or say plainly in the README and the
example that the middleware is HTTP-only and ship a dependency
(
Depends(guard_ws_check)) that a WebSocket route can use. The documentationroute is fine as a first step; the current state, where the example claims
coverage that does not exist, is the part that needs to change either way.
Fixed in 7.8.0
Fixed in fastapi-guard 7.8.0 (PyPI), merged in #121 (eb5f284).
guard_websocketis a FastAPI dependency that runs the ban list, the IP and country allow lists and the rate limit against the resolved client beforeaccept(), closing with 1008 on a policy violation; a client-less socket is rejected underfail_secure=True; Redis failures during the handshake followredis_fail_open/fail_securelike the HTTP pipeline (close code 1013).BaseHTTPMiddlewarenever runs for websocket scopes, so protection is opt-in per route:Depends(guard_websocket). Documented in the websockets tutorial; the example app wires it on/ws.