GHSA-pq2q-rcw4-3hr6 NATS: Pre-auth remote server crash via WebSocket frame length overflow in wsRead
BackgroundNATS.io is a high performance open source pub-sub distributed communication technology, built for the cloud, on-premise, IoT, and edge computing.When using WebSockets, a malicious client can trigger a server crash with crafted frames, before authentication.Problem DescriptionA missing sanity check on a WebSockets frame could trigger a server panic in the nats-server.  This happens before authentication, and so is exposed to anyone who can connect to the websockets port.Affected versionsVersion 2 from v2.2.0 onwards, prior to v2.11.14 or v2.12.5WorkaroundsThis only affects deployments which use WebSockets and which expose the network port to untrusted end-points.  If able to do so, a defense in depth of restricting either of these will mitigate the attack.SolutionUpgrade the NATS server to a fixed version.CreditsThis was reported to the NATS maintainers by GitHub user Mistz1.
Also independently reported by GitHub user jiayuqi7813.Report by @Mistz1SummaryAn unauthenticated remote attacker can crash the entire nats-server process by sending a single malicious WebSocket frame (15 bytes after the HTTP upgrade handshake). The server fails to validate the RFC 6455 §5.2 requirement that the most significant bit of a 64-bit extended payload length must be zero. The resulting `uint64` → `int` conversion produces a negative value, which bypasses the bounds clamp and triggers an unrecovered `panic` in the connection's goroutine — killing the entire server process and disconnecting all clients. This affects all platforms (64-bit and 32-bit).Details**Vulnerable code:** [`server/websocket.go` line 278](https://github.com/nats-io/nats-server/blob/a69f51f/server/websocket.go#L278)When a WebSocket frame uses the 64-bit extended payload length (length code 127), the server reads 8 bytes and casts the raw `uint64` directly to `int` with no validation. RFC 6455 §5.2 states: *"the most significant bit MUST be 0"* — but nats-server never checks this.**Attack chain:**The attacker sends a WebSocket frame with the MSB set in the 64-bit length field (e.g., `0x8000000000000001`).At line 278, `int(0x8000000000000001)` produces `-9223372036854775807` on 64-bit Go (two's complement reinterpretation — Go does not panic on integer conversion overflow).`r.rem` is now negative. At line 307–311, the bounds clamp fails:The addition `pos + n` wraps to a negative value (Go signed integer overflow is defined behavior — it wraps silently). Since the negative result is never greater than `max`, the clamp is skipped. The slice expression at line 311 reaches the Go runtime bounds check, which panics.There is **no `defer recover()`** anywhere in the goroutine chain:[`startGoRoutine`](https://github.com/nats-io/nats-server/blob/a69f51f/server/server.go#L4076-L4079): `go func() { f() }()` — no recovery[`readLoop`](https://github.com/nats-io/nats-server/blob/a69f51f/server/client.go#L1387-L1394): defer only does cleanup — no recoveryThe unrecovered panic propagates to Go's runtime, which calls `os.Exit(2)`. The **entire nats-server process terminates**.The WebSocket frame is parsed in `wsRead()` called from `readLoop()`, which starts immediately after the HTTP upgrade — **before any NATS CONNECT authentication**. No credentials are required.**Why 15 bytes, not 14:** The 14-byte frame header (opcode + length + mask key) exactly fills the read buffer on the first call, so `pos == max` and the payload loop at line 303 (`if pos < max`) is skipped. The poisoned `r.rem` persists in the `wsReadInfo` struct. One additional byte of "payload" is needed so that `pos < max` on either the same or next read, entering the panic path at line 311.PoC**Server configuration** (`test-ws.conf`):**Start the server:****Exploit** (`poc_ws_crash.go`):**Run:****Observed server output before termination:****Tested against:** nats-server v2.14.0-dev (commit `a69f51f`), Go 1.25.7, linux/amd64.Impact**Vulnerability type:** Pre-authentication remote denial of service (full process crash).**Who is impacted:** Any nats-server deployment with WebSocket listeners enabled (`websocket { ... }` in config), including MQTT-over-WebSocket. This is an increasingly common configuration for browser-based and IoT clients. The attacker needs only TCP access to the WebSocket port — no credentials, no valid NATS client, no TLS client certificate.**Severity:** A single unauthenticated TCP connection sending 15 bytes crashes the entire server process. All connected clients (NATS, WebSocket, MQTT, cluster routes, gateways, leaf nodes) are immediately disconnected. JetStream in-flight acknowledgments are lost and Raft consensus is disrupted in clustered deployments. The attack is repeatable on every server restart.**Affected platforms:** All — confirmed on 64-bit (linux/amd64); 32-bit platforms (linux/386, linux/arm) are also affected with additional frame-desync consequences.( NATS retains the original external report below the cut, with exploit details.
This issue was also independently reported by GitHub user @jiayuqi7813 before publication; they provided a Python exploit.)
CVSS v3.1 score: 7.5