GHSA-58xq-375p-x9cm: 
Python Análise e mitigação de vulnerabilidades

Summary

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.

Root Cause

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"

Impact

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.

Proof of Concept


# 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))
PY
thread '<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 granian

Attack Chain

  1. Exposure: A remote client sends a standard WebSocket upgrade request (Connection: 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.
  2. Control: The upgrade is detected at 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.
  3. Path: 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.
  4. Guard: There is no framework requirement that the application reject the connection explicitly, and no validation of the rejection status. The done callback at src/rsgi/callbacks.rs:104 unconditionally calls close(None), and close substitutes the invalid default status 0 at src/rsgi/io.rs:404.
  5. Primitive: The rejection response is built at 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.
  6. Result: The worker process is killed and all its connections are dropped. Repeated requests sustain the denial of service, and the error-handling callback triggers the same panic.

Bypass Evidence

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.

Affected Versions

  • Ecosystem: pip
  • Package: granian
  • Confirmed affected range: <= 2.8.5
  • Latest release checked: 2.8.5 (PyPI)
  • Fix status: not fixed The default close status of 0 and the unguarded rejection builder are present in the latest published release (2.8.5, commit 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.

Suggested Fix

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.


Origem: NVD

Relacionado Python Vulnerabilidades:

CVE ID

Gravidade

Pontuação

Tecnologias

Nome do componente

Exploit CISA KEV

Tem correção

Data de publicação

CVE-2026-113663HIGH8.8
  • Python logoPython
  • autogen
NãoSimNov 05, 2026
CVE-2026-58273HIGH8.1
  • Python logoPython
  • rucio
NãoSimNov 05, 2026
CVE-2026-112938MEDIUM5.8
  • Python logoPython
  • litellm-fips
NãoSimNov 05, 2026
GHSA-6p33-92hw-4v86MEDIUM4
  • Python logoPython
  • extract-msg
NãoSimNov 06, 2026
CVE-2026-112913LOW3.3
  • Python logoPython
  • nemo
NãoSimNov 05, 2026

Avaliação Gratuita de Vulnerabilidades

Avalie a sua postura de segurança na nuvem

Avalie suas práticas de segurança em nuvem em 9 domínios de segurança para avaliar seu nível de risco e identificar lacunas em suas defesas.

Avaliação de solicitação

Recursos adicionais da Wiz

Marque uma demonstração personalizada

Pronto para ver a Wiz em ação?

"A melhor experiência do usuário que eu já vi, fornece visibilidade total para cargas de trabalho na nuvem."
David EstlickCISO
"A Wiz fornece um único painel de vidro para ver o que está acontecendo em nossos ambientes de nuvem."
Adam FletcherDiretor de Segurança
"Sabemos que se a Wiz identifica algo como crítico, na verdade é."
Greg PoniatowskiChefe de Gerenciamento de Ameaças e Vulnerabilidades