ip4defrag: build() emits a reassembled IPv4 layer whose Length field lies about its own Payload #6
Labels
No labels
core
cpu-dos
critical
dos
evasion
has-poc
high
integer-overflow
ip4defrag
layers
low
medium
memory-exhaustion
other
panic
pcapgo
pentest-2026-08
rce
tcpassembly
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
noi/gopacket#6
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: medium ·
ip4defrag/defrag.go:283-320(fragmentList.build)build()assemblesfinalfrom whatever bytes each fragment actually carries, but sets the output header'sLengthfromf.Highest, which was accumulated from the declaredip.Lengthof each fragment. When any fragment was captured short — snaplen truncation, or a deliberately short frame — the two disagree, andbuild()returns success.The caller receives a
*layers.IPv4that looks like a normal, fully reassembled datagram and is internally inconsistent.Reproduction
Two non-overlapping fragments. The first declares 1480 payload bytes and carries 4:
No error, no
Truncatedflag on the result, no signal of any kind.Length=1488, payload 12 bytes — a 124× overstatement.Note this needs no overlap at all, so it is not blocked by the fixes for #1 or #2 — it goes through
build()'s ordinary contiguous path.Impact
The truncation is introduced by the capture, which is normal and expected. What
ip4defragdoes is launder it: an inconsistency that a consumer could have detected on the raw fragment (ip.Lengthvslen(ip.Payload), plusTruncatedmetadata) is re-emitted as a fresh, apparently-valid datagram with the metadata gone.Downstream consequences, in increasing order of severity:
Lengthover-reports by up to two orders of magnitude — cheap traffic-graph poisoning.Total_Lengthdoes not match the frame.payload[:ip.Length-ip.IHL*4], the single most natural thing to write — panics. gopacket's ownip4defragis not aDecoder, so nothing on this path is insideNewPacket'srecover().Fix
Set the output length from what was actually assembled, and refuse to return a datagram that does not match its own accounting:
The stricter and better fix is the one in #1: reject a fragment whose payload is shorter than it claims in
securityChecks(), so a short capture never enters a fragment list at all. With that in place this divergence cannot arise. The assertion above is still worth keeping as a backstop, since it is the invariantbuild()is supposed to maintain.Verified against
b7d9dbdon Go 1.24.4. PoC:defrag_more, case D.Independent verification — present in
gopacket/gopacket v1.7.0; one caller-side mitigation worth documentingThe defect is present verbatim in the maintained fork:
build()assemblesfinalfrom captured bytes but sets the output header'sLengthfromf.Highest, accumulated from each fragment's declaredip.Length. Confirmed by source inspection.The analysis in this issue is correct, and the "laundering" framing is the right one — the inconsistency is detectable on the raw fragment (
ip.Lengthvslen(ip.Payload), plusTruncated) and is re-emitted with that metadata gone.A mitigation that happens to work, and why it shouldn't be relied on
The consumer audited alongside these issues turns out to be immune to this one, by accident rather than design. Rather than passing the
*layers.IPv4fromDefragIPv4downstream, it re-serialises the reassembled datagram back into a synthetic frame before decoding:FixLengths: truerecomputesTotal_Lengthfrom the payload actually present, so the lyingLengthnever escapes the defragmentation step. The consumer also derives its byte accounting fromlen(reassembled)rather than the header field, so the traffic-graph poisoning described here does not land either.Two things follow:
FixLengthsinstead of consuming the returned layer directly. It is a two-line change and it closes the whole class.out.Length, or sliceout.Payload[:out.Length-uint16(out.IHL)*4]— gets the full impact, including the panic this issue predicts in its third bullet. The fix belongs inbuild().Concurring on the "no overlap required" point
Confirming this independently, because it affects remediation sequencing: this reaches
build()'s ordinary contiguous path, so neither the fix for #1 nor the fix for #2 closes it. Of the four ip4defrag issues, this is the one most likely to be assumed fixed by the others and left open.Suggested addition to the proposed fix
The proposed
if int(f.Highest) != len(final)check is right. Worth pairing it with propagating truncation rather than only rejecting it — if any contributing fragment hadTruncatedset, the assembled result should carry that flag, so a caller that wants the bytes anyway can still tell they are incomplete. Right now the only options the fix offers are "valid datagram" or "error", and a capture with snaplen truncation is a legitimate, expected input rather than an attack.Verified against
gopacket/gopacket v1.7.0on Go 1.24.4.