Observed behavior
Calling nats.DecodeHeadersMsg with a header whose inline status token is shorter
than 3 characters causes an unrecovered panic: runtime error: slice bounds out of range, crashing the client process immediately.
The panic is in DecodeHeadersMsg (nats.go). The guard len(status) != statusLen
is intended to split a status code from a trailing description (e.g.
503 No Responders). Because it uses != instead of >, it also fires when the
status token is shorter than 3 characters, and immediately attempts status[3:]
on a 0-, 1-, or 2-byte string — an out-of-bounds slice:
// nats.go — DecodeHeadersMsg
status := strings.TrimSpace(l[hdrPreEnd:])
if len(status) != statusLen { // BUG: fires when len < 3 too
description = strings.TrimSpace(status[statusLen:]) // status[3:] → panic if len < 3
status = status[:statusLen]
}
panic: runtime error: slice bounds out of range [3:1]
goroutine 1 [running]:
github.com/nats-io/nats.go.DecodeHeadersMsg(...)
nats.go:4443
| Header first line |
Trimmed status token |
Result |
NATS/1.05\r\n |
"5" (len 1) |
panic: slice bounds out of range [3:1] |
NATS/1.0 4\r\n |
"4" (len 1) |
panic: slice bounds out of range [3:1] |
NATS/1.0 41\r\n |
"41" (len 2) |
panic: slice bounds out of range [3:2] |
NATS/1.0 200\r\n |
"200" (len 3) |
ok |
Expected behavior
DecodeHeadersMsg should return an error (e.g. ErrBadHeaderMsg) when the inline
status token is malformed, rather than panicking. A status token shorter than 3
characters is invalid and should be rejected gracefully without crashing the process.
Suggested fix : one character change:
- if len(status) != statusLen {
+ if len(status) > statusLen {
description = strings.TrimSpace(status[statusLen:])
status = status[:statusLen]
}
With >, the description split only runs when there is genuinely content after the
3-char status code. Tokens of length 0, 1, or 2 fall through and can be surfaced
as ErrBadHeaderMsg.
Server and client version
- nats.go client:
v1.39.1 (current latest release), also reproduced on main HEAD
- nats-server: any, the panic is purely in client-side header decoding, no server version dependency
- Go toolchain:
go1.25 (linux/amd64), also reproduced on go1.21
Host environment
- OS: Linux (Ubuntu 24.04,
x86_64)
- Architecture: amd64
- Reproduces in a standalone Go program with no running NATS server
Steps to reproduce
mkdir repro && cd repro
go mod init repro
go get github.com/nats-io/nats.go@v1.39.1
main.go:
package main
import (
"fmt"
"github.com/nats-io/nats.go"
)
func main() {
cases := [][]byte{
[]byte("NATS/1.05\r\n\r\n"), // status "5" (len 1) → panic [3:1]
[]byte("NATS/1.0 4\r\n\r\n"), // status "4" (len 1) → panic [3:1]
[]byte("NATS/1.0 41\r\n\r\n"), // status "41" (len 2) → panic [3:2]
[]byte("NATS/1.0 200\r\n\r\n"), // status "200" (len 3) → ok
}
for _, c := range cases {
func() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("input %-26q PANIC: %v\n", c, r)
return
}
fmt.Printf("input %-26q ok\n", c)
}()
_, _ = nats.DecodeHeadersMsg(c)
}()
}
}
$ go run .
input "NATS/1.05\r\n\r\n" PANIC: runtime error: slice bounds out of range [3:1]
input "NATS/1.0 4\r\n\r\n" PANIC: runtime error: slice bounds out of range [3:1]
input "NATS/1.0 41\r\n\r\n" PANIC: runtime error: slice bounds out of range [3:2]
input "NATS/1.0 200\r\n\r\n" ok
Discovery note: this bug was found using Zorya,
an open-source concolic execution engine for Go binaries developed by Ledger Donjon.
Zorya ran in function mode on a harness reproducing the DecodeHeadersMsg status-split
logic, symbolized the status-token length, and solved the path constraint
len(status) != 3 ∧ len(status) < 3 to produce a witness value that drives execution
to runtime.panicSliceB:
[*] SATISFIABLE STATE FOUND (elapsed 94.88s)
Instruction Address: 0x4b7284 ← cmp $0x3,%rcx ; jne (len(status) != statusLen)
Panic Address: 0x4b72b9 ← call runtime.panicSliceB (status[statusLen:])
The input 'n' must be 0 ← status-token length < statusLen; class n ∈ {0, 1, 2}
Network reachability note: DecodeHeadersMsg is called by the client when
processing HMSG frames delivered by the server. The status line in those frames is
server-controlled, so a malicious or compromised NATS server can crash any
header-aware nats.go client with a single malformed frame. The function is also
exported and may be called directly on untrusted bytes by application code.
Observed behavior
Calling
nats.DecodeHeadersMsgwith a header whose inline status token is shorterthan 3 characters causes an unrecovered
panic: runtime error: slice bounds out of range, crashing the client process immediately.The panic is in
DecodeHeadersMsg(nats.go). The guardlen(status) != statusLenis intended to split a status code from a trailing description (e.g.
503 No Responders). Because it uses!=instead of>, it also fires when thestatus token is shorter than 3 characters, and immediately attempts
status[3:]on a 0-, 1-, or 2-byte string — an out-of-bounds slice:
NATS/1.05\r\n"5"(len 1)NATS/1.0 4\r\n"4"(len 1)NATS/1.0 41\r\n"41"(len 2)NATS/1.0 200\r\n"200"(len 3)Expected behavior
DecodeHeadersMsgshould return an error (e.g.ErrBadHeaderMsg) when the inlinestatus token is malformed, rather than panicking. A status token shorter than 3
characters is invalid and should be rejected gracefully without crashing the process.
Suggested fix : one character change:
With
>, the description split only runs when there is genuinely content after the3-char status code. Tokens of length 0, 1, or 2 fall through and can be surfaced
as
ErrBadHeaderMsg.Server and client version
v1.39.1(current latest release), also reproduced onmainHEADgo1.25(linux/amd64), also reproduced ongo1.21Host environment
x86_64)Steps to reproduce
main.go:Discovery note: this bug was found using Zorya,
an open-source concolic execution engine for Go binaries developed by Ledger Donjon.
Zorya ran in function mode on a harness reproducing the
DecodeHeadersMsgstatus-splitlogic, symbolized the status-token length, and solved the path constraint
len(status) != 3 ∧ len(status) < 3to produce a witness value that drives executionto
runtime.panicSliceB:Network reachability note:
DecodeHeadersMsgis called by the client whenprocessing
HMSGframes delivered by the server. The status line in those frames isserver-controlled, so a malicious or compromised NATS server can crash any
header-aware nats.go client with a single malformed frame. The function is also
exported and may be called directly on untrusted bytes by application code.