NATS Server panic via malicious compression on leafnode port
Description
NATS-Server is a High-Performance server for NATS.io, a cloud and edge native messaging system. Prior to versions 2.11.14 and 2.12.5, if the nats-server has the "leafnode" configuration enabled (not default), then anyone who can connect can crash the nats-server by triggering a panic. This happens pre-authentication and requires that compression be enabled (which it is, by default, when leafnodes are used). Versions 2.11.14 and 2.12.5 contain a fix. As a workaround, disable compression on the leafnode port.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/nats-io/nats-server/v2Go | < 2.11.14 | 2.11.14 |
github.com/nats-io/nats-server/v2Go | >= 2.12.0-RC.1, < 2.12.5 | 2.12.5 |
github.com/nats-io/nats-serverGo | >= 0 | — |
Affected products
1- Range: < 2.11.14
Patches
1a1488de6f2baFix panic on LS protocol when compression enabled
2 files changed · +241 −1
server/leafnode.go+17 −1 modified@@ -2760,6 +2760,14 @@ func (c *client) processLeafSub(argo []byte) (err error) { } acc := c.acc + // Guard against LS+ arriving before CONNECT has been processed, which + // can happen when compression is enabled. + if acc == nil { + c.mu.Unlock() + c.sendErr("Authorization Violation") + c.closeConnection(ProtocolViolation) + return nil + } // Check if we have a loop. ldsPrefix := bytes.HasPrefix(sub.subject, []byte(leafNodeLoopDetectionSubjectPrefix)) @@ -2876,7 +2884,6 @@ func (c *client) processLeafUnsub(arg []byte) error { // Indicate any activity, so pub and sub or unsubs. c.in.subs++ - acc := c.acc srv := c.srv c.mu.Lock() @@ -2885,6 +2892,15 @@ func (c *client) processLeafUnsub(arg []byte) error { return nil } + acc := c.acc + // Guard against LS- arriving before CONNECT has been processed. + if acc == nil { + c.mu.Unlock() + c.sendErr("Authorization Violation") + c.closeConnection(ProtocolViolation) + return nil + } + spoke := c.isSpokeLeafNode() // We store local subs by account and subject and optionally queue name. // LS- will have the arg exactly as the key.
server/leafnode_test.go+224 −0 modified@@ -10891,3 +10891,227 @@ func TestLeafNodesBasicTokenAuth(t *testing.T) { checkLeafNodeConnected(t, hub) checkLeafNodeConnected(t, leaf) } + +func TestLeafNodeNoAccPanicOnLeafSubBeforeConnect(t *testing.T) { + o := DefaultOptions() + o.LeafNode.Port = -1 + // Default compression is s2_auto, which causes the auth timer to use + // c.ping.tmr instead of c.atmr. This makes awaitingAuth() return false, + // allowing LS+ through the parser before CONNECT sets c.acc. + s := RunServer(o) + defer s.Shutdown() + + addr := fmt.Sprintf("127.0.0.1:%d", o.LeafNode.Port) + c, err := net.Dial("tcp", addr) + require_NoError(t, err) + defer c.Close() + + // Read the INFO. + br := bufio.NewReader(c) + c.SetReadDeadline(time.Now().Add(2 * time.Second)) + l, _, err := br.ReadLine() + require_NoError(t, err) + if !strings.HasPrefix(string(l), "INFO") { + t.Fatalf("Expected INFO, got %q", l) + } + + // Send LS+ without CONNECT first. This should not panic the server. + _, err = c.Write([]byte("LS+ test\r\n")) + require_NoError(t, err) + + // The server should close the connection. + c.SetReadDeadline(time.Now().Add(2 * time.Second)) + buf := make([]byte, 256) + for { + _, err = c.Read(buf) + if err != nil { + break + } + } + + // Make sure the server is still running. + s.mu.Lock() + shutdown := s.isShuttingDown() + s.mu.Unlock() + if shutdown { + t.Fatal("Server should not have shutdown") + } +} + +func TestLeafNodeNoAccPanicOnLeafUnsubBeforeConnect(t *testing.T) { + o := DefaultOptions() + o.LeafNode.Port = -1 + s := RunServer(o) + defer s.Shutdown() + + addr := fmt.Sprintf("127.0.0.1:%d", o.LeafNode.Port) + c, err := net.Dial("tcp", addr) + require_NoError(t, err) + defer c.Close() + + // Read the INFO. + br := bufio.NewReader(c) + c.SetReadDeadline(time.Now().Add(2 * time.Second)) + l, _, err := br.ReadLine() + require_NoError(t, err) + if !strings.HasPrefix(string(l), "INFO") { + t.Fatalf("Expected INFO, got %q", l) + } + + // Send LS- without CONNECT first. This should not panic the server. + _, err = c.Write([]byte("LS- test\r\n")) + require_NoError(t, err) + + // The server should close the connection. + c.SetReadDeadline(time.Now().Add(2 * time.Second)) + buf := make([]byte, 256) + for { + _, err = c.Read(buf) + if err != nil { + break + } + } + + // Make sure the server is still running. + s.mu.Lock() + shutdown := s.isShuttingDown() + s.mu.Unlock() + if shutdown { + t.Fatal("Server should not have shutdown") + } +} + +func TestLeafNodeLeafSubBeforeConnectCompressionEffect(t *testing.T) { + for _, test := range []struct { + name string + compression string + // With compression off, the auth timer (c.atmr) is set, so + // awaitingAuth() returns true and the parser blocks LS+ with + // an auth violation. With compression on (default s2_auto), + // c.ping.tmr is used instead, awaitingAuth() returns false, + // and LS+ reaches processLeafSub where our nil acc guard + // catches it. Both paths send the same "Authorization Violation" + // error for consistency. + }{ + {"compression off", CompressionOff}, + {"compression s2_auto", CompressionS2Auto}, + } { + t.Run(test.name, func(t *testing.T) { + o := DefaultOptions() + o.LeafNode.Port = -1 + o.LeafNode.Compression.Mode = test.compression + s := RunServer(o) + defer s.Shutdown() + + addr := fmt.Sprintf("127.0.0.1:%d", o.LeafNode.Port) + c, err := net.Dial("tcp", addr) + require_NoError(t, err) + defer c.Close() + + br := bufio.NewReader(c) + c.SetReadDeadline(time.Now().Add(2 * time.Second)) + l, _, err := br.ReadLine() + require_NoError(t, err) + if !strings.HasPrefix(string(l), "INFO") { + t.Fatalf("Expected INFO, got %q", l) + } + + // Send LS+ without CONNECT. + _, err = c.Write([]byte("LS+ test\r\n")) + require_NoError(t, err) + + // Read the error response before the connection is closed. + c.SetReadDeadline(time.Now().Add(2 * time.Second)) + l, _, err = br.ReadLine() + require_NoError(t, err) + errMsg := string(l) + + if !strings.Contains(errMsg, "Authorization Violation") { + t.Fatalf("Expected auth violation error, got %q", errMsg) + } + + // Make sure the server is still running. + s.mu.Lock() + shutdown := s.isShuttingDown() + s.mu.Unlock() + if shutdown { + t.Fatal("Server should not have shutdown") + } + }) + } +} + +func TestLeafNodeNoAccPanicOnLeafSubBeforeConnectOperatorMode(t *testing.T) { + // Setup operator JWT-based server with leafnode port. + // This confirms that even with full operator/JWT auth configured, + // a raw TCP connection can bypass auth and trigger the panic. + sysAcc, _ := nkeys.CreateAccount() + sysAccPub, _ := sysAcc.PublicKey() + + okp, _ := nkeys.FromSeed(oSeed) + opub, _ := okp.PublicKey() + + // Create operator claim with system account. + oc := jwt.NewOperatorClaims(opub) + oc.SystemAccount = sysAccPub + operatorJwt, err := oc.Encode(okp) + require_NoError(t, err) + + // Create the system account JWT. + sysAccClaim := jwt.NewAccountClaims(sysAccPub) + sysAccJwt, err := sysAccClaim.Encode(okp) + require_NoError(t, err) + + conf := createConfFile(t, []byte(fmt.Sprintf(` + port: -1 + server_name: OP + operator: %s + system_account: %s + resolver: MEMORY + resolver_preload: { + %s: %s + } + leafnodes { + listen: "127.0.0.1:-1" + } + `, operatorJwt, sysAccPub, sysAccPub, sysAccJwt))) + s, opts := RunServerWithConfig(conf) + defer s.Shutdown() + + addr := fmt.Sprintf("127.0.0.1:%d", opts.LeafNode.Port) + c, err := net.Dial("tcp", addr) + require_NoError(t, err) + defer c.Close() + + // Read the INFO. + br := bufio.NewReader(c) + c.SetReadDeadline(time.Now().Add(2 * time.Second)) + l, _, err := br.ReadLine() + require_NoError(t, err) + if !strings.HasPrefix(string(l), "INFO") { + t.Fatalf("Expected INFO, got %q", l) + } + + // Send LS+ without CONNECT first, bypassing JWT auth entirely. + // Without the fix this would panic on nil c.acc dereference. + _, err = c.Write([]byte("LS+ test\r\n")) + require_NoError(t, err) + + // The server should close the connection. + c.SetReadDeadline(time.Now().Add(2 * time.Second)) + buf := make([]byte, 256) + for { + _, err = c.Read(buf) + if err != nil { + break + } + } + + // Make sure the server is still running. + s.mu.Lock() + shutdown := s.isShuttingDown() + s.mu.Unlock() + if shutdown { + t.Fatal("Server should not have shutdown") + } +}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-52jh-2xxh-pwh6ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-29785ghsaADVISORY
- advisories.nats.io/CVE/secnote-2026-04.txtghsax_refsource_MISCWEB
- github.com/nats-io/nats-server/commit/a1488de6f2ba6e666aef0f9cce0016f7f167d6a8ghsax_refsource_MISCWEB
- github.com/nats-io/nats-server/security/advisories/GHSA-52jh-2xxh-pwh6ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.