Skip to content

bug: DecodeHeadersMsg slice out-of-range panic on short status token #2100

Description

@kajaaz

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    defectSuspected defect such as a bug or regression

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions