medium

CVE-2026-54345

Go · github.com/gopacket/gopacket

Summary

GoPacket's Diameter AVP decoder: uint32 underflow on vendor header size leads to unbounded ~4 GiB allocation (unauthenticated remote DoS)

Severity
medium
EPSS
0.8% (p56)
CWE
CWE-191, CWE-770
Also known as
GHSA-6r28-9ppf-4hj5
Published
2026-07-28
Updated
2026-07-28

Advisory details

Summary

The Diameter AVP decoder in github.com/gopacket/gopacket computes dataLength := avp.Length - uint32(headerSize) without first ensuring avp.Length >= headerSize. When the Vendor flag is set, headerSize is 12, but the only length guard upstream rejects avp.Length < 8. An AVP with the Vendor flag set and a 24-bit Length field of 8, 9, 10, or 11 therefore underflows the uint32 subtraction to ~4,294,967,292, which is passed straight to make([]byte, dataLength). A single 32-byte Diameter message forces a ~4 GiB allocation; a short burst of such messages exhausts memory and OOM-kills memory-constrained collectors. This is an unauthenticated remote denial of service (CWE-191 integer underflow -> CWE-770 unbounded allocation).

Root cause (file:line @ v1.6.0)

layers/diameter_avp_decoders.go, decodeDiameterAVP:

avp.Length = uint32(data[5])<<16 | uint32(data[6])<<8 | uint32(data[7]) // 24-bit wire value

if avp.Length < 8 {                       // only rejects < 8
    return DiameterAVP{}, 0, fmt.Errorf("invalid AVP length: %d", avp.Length)
}

headerSize := 8
dataOffset := 8
if avp.Flags.Vendor {                     // Vendor flag = wire bit data[4] & 0x80
    if len(data) < 12 { ... }
    avp.VendorID = binary.BigEndian.Uint32(data[8:12])
    headerSize = 12                       // header is now 12, but only >= 8 was checked
    dataOffset = 12
}

paddedLength := avp.Length                // equals avp.Length; for avp.Length <= 12
if avp.Length%4 != 0 { paddedLength = avp.Length + (4 - avp.Length%4) }
if uint32(len(data)) < paddedLength {     // only requires ~12 bytes present
    return DiameterAVP{}, 0, fmt.Errorf("AVP data truncated: ...")
}

dataLength := avp.Length - uint32(headerSize)  // 8 - 12 = uint32 underflow = 4294967292
avp.Data = make([]byte, dataLength)            // make([]byte, ~4.29e9) ~= 4 GiB
copy(avp.Data, data[dataOffset:dataOffset+int(dataLength)])  // out-of-bounds slice -> panic

For avp.Length in {8, 9, 10, 11} with the Vendor flag set: the avp.Length < 8 guard passes, paddedLength == avp.Length so only avp.Length bytes must be present, and dataLength = avp.Length - 12 underflows the uint32. The allocation size is determined entirely by the attacker-supplied 3-byte Length field plus a single flag bit. The make executes before the copy, so the multi-gigabyte allocation is requested regardless of whether the copy later panics.

Reachability (remote attacker -> sink)

LayerTypeDiameter is a registered decoder (layertypes.go:159, RegisterLayerType(154, ... decodeDiameter)):

decodeDiameter -> (*Diameter).DecodeFromBytes (diameter.go:118) -> parses the 20-byte header -> avpData := data[20:d.MessageLength] -> AVP loop decodeDiameterAVP(avpData) (diameter.go:158) -> sink at diameter_avp_decoders.go:57.

Diameter (RFC 6733) is a TCP/SCTP base protocol used for AAA and telecom/5G signaling. Any service that parses Diameter with gopacket (packet collectors, signaling monitors, IDS/analysis tooling) processes attacker-sent or attacker-forwarded Diameter messages with no authentication involved, so any host able to deliver such a message to the parser reaches the sink. The same path is reached via gopacket.NewPacket(data, LayerTypeDiameter, ...). The Diameter layer is specific to this gopacket fork (the original google/gopacket has no Diameter layer), so there is no upstream sibling fix.

Impact

Unauthenticated remote denial of service via memory exhaustion. Each malicious 32-byte Diameter message requests a ~4 GiB allocation (amplification ~1.34e8x over the input). There is no memory corruption and no code execution -- the impact is resource exhaustion / process termination. Severity assessed as Medium (unauthenticated remote DoS, no memory-safety violation).

Note on consumer behavior (measured end-to-end against a deployed collector, see PoC):

In all cases the underlying defect is the same unbounded ~4 GiB allocation driven by an attacker-controlled field; the only variable is how many messages it takes to exhaust a given memory limit.

Proof of Concept

This PoC is an end-to-end test against a real deployed Diameter collector. A minimal but realistic TCP collector (built on the public gopacket API) runs inside a hard-capped 256 MB container; an independent client process sends real malicious Diameter messages over a real TCP socket; the collector process is then observed to die. A benign message is used as a negative control. The harness pins github.com/gopacket/gopacket@v1.6.0 (the sink is confirmed at the v1.6.0 tag, layers/diameter_avp_decoders.go:56-58).

Collector (real TCP Diameter collector)

// collector.go — accepts a TCP connection, reads one Diameter message (framed by
// the 24-bit Message Length in the base header), builds a gopacket.Packet rooted
// at LayerTypeDiameter and accesses the layer, which drives the registered
// Diameter decoder over the attacker-controlled bytes.
package main

import (
	"fmt"
	"io"
	"net"
	"os"

	"github.com/gopacket/gopacket"
	"github.com/gopacket/gopacket/layers"
)

func readDiameterMessage(conn net.Conn) ([]byte, error) {
	hdr := make([]byte, 20)
	if _, err := io.ReadFull(conn, hdr); err != nil {
		return nil, err
	}
	msgLen := uint32(hdr[1])<<16 | uint32(hdr[2])<<8 | uint32(hdr[3])
	if msgLen < 20 {
		return hdr, nil
	}
	full := make([]byte, msgLen)
	copy(full, hdr)
	if _, err := io.ReadFull(conn, full[20:]); err != nil {
		return nil, err
	}
	return full, nil
}

func main() {
	ln, err := net.Listen("tcp", "0.0.0.0:3868")
	if err != nil {
		fmt.Fprintf(os.Stderr, "listen error: %v\n", err)
		os.Exit(1)
	}
	defer ln.Close()
	fmt.Printf("[collector] Diameter collector listening on tcp %s\n", ln.Addr())
	for {
		conn, err := ln.Accept()
		if err != nil {
			continue
		}
		func() {
			defer conn.Close()
			data, err := readDiameterMessage(conn)
			if err != nil {
				return
			}
			fmt.Printf("[collector] received %d-byte Diameter message from %s\n", len(data), conn.RemoteAddr())
			pkt := gopacket.NewPacket(data, layers.LayerTypeDiameter, gopacket.Default)
			if d, ok := pkt.Layer(layers.LayerTypeDiameter).(*layers.Diameter); ok {
				fmt.Printf("[collector] decoded Diameter: version=%d cmd=%d msgLen=%d avps=%d\n",
					d.Version, d.CommandCode, d.MessageLength, len(d.AVPs))
			} else {
				fmt.Printf("[collector] no Diameter layer decoded\n")
			}
		}()
	}
}

Client (independent process, real TCP socket, no gopacket dependency)

The client crafts a 20-byte Diameter base header followed by one vendor AVP whose 24-bit Length is avpLen. With the Vendor flag set, the decoder's headerSize becomes 12; for avpLen in {8,9,10,11} the dataLength = avpLen - 12 subtraction underflows. For the benign case avpLen >= 12 so the AVP carries avpLen-12 real bytes and parses cleanly.

// client.go —

References

Related advisories

Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.

Check my repo

Summarize with AI

ChatGPTClaudePerplexity

Sources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.