diff --git a/outside.go b/outside.go index 29607eae..4464acdf 100644 --- a/outside.go +++ b/outside.go @@ -422,16 +422,14 @@ func parseV6(data []byte, incoming bool, fp *firewall.Packet) error { if dataLen <= offset+1 { break } - - next = int(data[offset+1]+2) << 2 + next = (int(data[offset+1]) + 2) << 2 default: // Normal ipv6 header length processing if dataLen <= offset+1 { break } - - next = int(data[offset+1]+1) << 3 + next = (int(data[offset+1]) + 1) << 3 } if next <= 0 { diff --git a/outside_test.go b/outside_test.go index 042ccbb3..4a24cae5 100644 --- a/outside_test.go +++ b/outside_test.go @@ -640,3 +640,38 @@ func serializeAH(ah *layers.IPSecAH) []byte { return buf.Bytes() } + +// Test_newPacket_v6ExtHeaderOverflow is a regression test for the IPv6 extension-header +// length uint8 overflow in parseV6. A Destination-Options header with HdrExtLen=255 spans +// (255+1)*8 = 2048 bytes, so the real transport header sits at offset 2088. Before the fix +// the advance was computed in uint8 and wrapped to 0 (then clamped to 8), so the firewall +// read the transport header ~2KB too early from attacker-controlled option bytes while the +// host OS parses the real header, a firewall port/proto bypass. The fix makes parseV6 land +// on the same offset the host does. +func Test_newPacket_v6ExtHeaderOverflow(t *testing.T) { + p := &firewall.Packet{} + + const ( + hdrLen = 40 // IPv6 header + extLen = 2048 // (255+1)*8, the true Destination-Options header size + realTCPAt = hdrLen + extLen // 2088, where the host reads the transport header + forgedTCPAt = hdrLen + 8 // 48, where the pre-fix wrapped+clamped walk landed + ) + + pkt := make([]byte, realTCPAt+4) + pkt[0] = 0x60 // version 6 + pkt[6] = byte(layers.IPProtocolIPv6Destination) // NextHeader -> Destination Options + pkt[40] = byte(firewall.ProtoTCP) // Dest-Options NextHeader -> TCP + pkt[41] = 255 // HdrExtLen = 255 + + // Forged transport header at the pre-fix (wrong) offset: dst port 443. + binary.BigEndian.PutUint16(pkt[forgedTCPAt+2:forgedTCPAt+4], 443) + // Real transport header at the offset the host actually uses: dst port 22. + binary.BigEndian.PutUint16(pkt[realTCPAt+2:realTCPAt+4], 22) + + require.NoError(t, newPacket(pkt, true, p)) + assert.Equal(t, uint8(firewall.ProtoTCP), p.Protocol) + // LocalPort is the destination port for incoming traffic. It must be the real port (22) + // the host delivers to, not the forged 443 at the overflowed offset. + assert.Equal(t, uint16(22), p.LocalPort, "firewall must parse the real transport header, not the overflowed offset") +}