
Cloud Vulnerability DB
A community-led vulnerabilities database
Granian aborts the entire worker process when it receives a WebSocket upgrade request on an RSGI server that has WebSocket support enabled and the served application returns without explicitly accepting or closing the connection. In that case granian's own framework callback rejects the connection with a default close status of 0, which is not a valid HTTP status code. The rejection response is then built with ResponseBuilder::new().status(0 as u16).body(empty_body()).unwrap(), where the invalid status causes the builder to store an error and the unwrap() panics. Release builds compile with panic = "abort", so the panic terminates the worker process, dropping every concurrent connection. No authentication is required; a single standard WebSocket upgrade request to any endpoint that does not handle WebSockets kills the worker.
When an RSGI application finishes handling a WebSocket scope without calling accept() or close(), granian's done callback invokes close(None) to reject the connection. RSGIWebsocketProtocol::close substitutes a default status of 0 for the None case (status.unwrap_or(0)). That value, 0, is not a valid HTTP status code. The rejection response is then constructed from this framework-chosen status without validation, and the fallible ResponseBuilder result is discarded with unwrap(). Because 0 is invalid, the builder carries an error through body(), and unwrap() panics. The ASGI adapter does not have this defect: its equivalent rejection path hardcodes StatusCode::FORBIDDEN. The RSGI path instead trusts a framework default of 0.
Default status: src/rsgi/io.rs:404
let _ = tx.send((status.unwrap_or(0), consumed, handle));
Done callback: src/rsgi/callbacks.rs:24-27
$self.proto.get().close(None);
Panicking sink: src/rsgi/http.rs:94-98
ResponseBuilder::new()
.status(status as u16) // 0
.header(HK_SERVER, HV_SERVER)
.body(empty_body())
.unwrap(); // panics: status 0 is invalid
ASGI (safe) counterpart: src/asgi/http.rs:94
.status(StatusCode::FORBIDDEN)
Release profile: Cargo.toml:75
panic = "abort"An unauthenticated remote attacker can terminate a granian worker process with a single WebSocket upgrade request, dropping all connections served by that worker. The precondition is that the RSGI server runs with WebSocket support enabled (websockets_enabled=true, set via the --websockets flag or the worker configuration; the default is disabled). Once enabled, the request need only target any endpoint whose handler does not explicitly accept or close the WebSocket connection, which is the normal case for any endpoint that is not written to handle WebSockets. Exploitation is deterministic and requires no race or secret. The error-recovery path is also affected: if the application raises an exception while handling the WebSocket, granian's error callback likewise calls close(None), so the recovery itself panics. Repeating the request across workers removes availability of the service. There is no confidentiality or integrity impact.
# app.py (RSGI)
# An endpoint that does not handle WebSocket scopes: returns without accept()/close().
async def __rsgi__(scope, protocol):
if scope.proto == "http":
protocol.response_empty(200, [])
# For a "ws" scope, the handler returns without accept()/close().Enable WebSockets and send one standard upgrade request:
granian --interface rsgi --websockets app:__rsgi__ --host 127.0.0.1 --port 8000 &
python - <<'PY'
import base64, os, socket
key = base64.b64encode(os.urandom(16)).decode()
req = (
"GET / HTTP/1.1\r\nHost: 127.0.0.1:8000\r\n"
"Upgrade: websocket\r\nConnection: Upgrade\r\n"
f"Sec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n"
).encode()
with socket.create_connection(("127.0.0.1", 8000), timeout=5) as s:
s.sendall(req)
print(s.recv(4096))
PYthread '<unnamed>' panicked at src/rsgi/http.rs:98:
called `Result::unwrap()` on an `Err` value: http::Error
[ERROR] Unexpected exit from worker-1
[INFO] Shutting down granianConnection: Upgrade, Upgrade: websocket, Sec-WebSocket-Key, Sec-WebSocket-Version: 13) to any endpoint on an RSGI server started with WebSocket support enabled. No authentication is required.src/rsgi/http.rs:76 and the handshake is validated at src/ws.rs:110. These checks require only the standard headers, which any WebSocket client provides.call_ws at src/rsgi/callbacks.rs:138 invokes the application callback. The application returns without calling accept() or close(), which is the normal outcome for any endpoint not written to handle WebSockets.src/rsgi/callbacks.rs:104 unconditionally calls close(None), and close substitutes the invalid default status 0 at src/rsgi/io.rs:404.src/rsgi/http.rs:94-98 with status 0. StatusCode::from_u16(0) returns Err(InvalidStatusCode), the builder stores the error, body() returns an error, and unwrap() panics. Under panic = "abort" (Cargo.toml:75) the panic aborts the worker process.The protection that should exist is a valid rejection status and validation before response construction; neither is present on the RSGI path. RSGIWebsocketProtocol::close(None) produces status 0 (src/rsgi/io.rs:404), and the rejection builder at src/rsgi/http.rs:94-98 converts it with no range check. Hyper's ResponseBuilder::status(0u16) invokes StatusCode::from_u16(0), which returns Err(InvalidStatusCode); the stored error propagates through body() and unwrap() panics. An application can avoid the panic only by explicitly calling accept() or close(valid_status) for every WebSocket scope, which is application-level defense and not provided by the framework. The ASGI adapter demonstrates the correct behavior by hardcoding StatusCode::FORBIDDEN at src/asgi/http.rs:94, confirming this is an RSGI-specific omission. The previously published subprotocol-header panic for this project changed src/asgi/utils.rs only and does not touch this RSGI rejection path.
pipgranian<= 2.8.52.8.5 (PyPI)59cbc26734634c9e9089180f3fd4469adde214de). Commits since the earliest inspected tag refactored the RSGI WebSocket transport locking and disconnection guards but did not change the default close status or add validation. No published release contains a fix. The introduction boundary could not be established from the available tags, so the lower bound is left open.Reject unaccepted RSGI WebSocket connections with a valid HTTP status instead of the default 0, matching the ASGI path that uses StatusCode::FORBIDDEN. Change RSGIWebsocketProtocol::close so that a None status maps to a valid rejection code, or validate the status in the rejection builder at src/rsgi/http.rs:94-98 and handle the error rather than calling unwrap(). The fix must also cover the error-recovery path, which calls the same close(None) through the error callback, so that an application exception during WebSocket handling cannot abort the worker.
Reported by zx.
Source: NVD
Free Vulnerability Assessment
Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.
Get a personalized demo
"Best User Experience I have ever seen, provides full visibility to cloud workloads."
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
"We know that if Wiz identifies something as critical, it actually is."