Skip to content

Commit c1eea11

Browse files
authored
fix firewall port/proto bypass in parseV6 from uint8 extension-header length overflow (#1789)
1 parent 1e66c0d commit c1eea11

2 files changed

Lines changed: 37 additions & 4 deletions

File tree

outside.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -422,16 +422,14 @@ func parseV6(data []byte, incoming bool, fp *firewall.Packet) error {
422422
if dataLen <= offset+1 {
423423
break
424424
}
425-
426-
next = int(data[offset+1]+2) << 2
425+
next = (int(data[offset+1]) + 2) << 2
427426

428427
default:
429428
// Normal ipv6 header length processing
430429
if dataLen <= offset+1 {
431430
break
432431
}
433-
434-
next = int(data[offset+1]+1) << 3
432+
next = (int(data[offset+1]) + 1) << 3
435433
}
436434

437435
if next <= 0 {

outside_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,3 +640,38 @@ func serializeAH(ah *layers.IPSecAH) []byte {
640640

641641
return buf.Bytes()
642642
}
643+
644+
// Test_newPacket_v6ExtHeaderOverflow is a regression test for the IPv6 extension-header
645+
// length uint8 overflow in parseV6. A Destination-Options header with HdrExtLen=255 spans
646+
// (255+1)*8 = 2048 bytes, so the real transport header sits at offset 2088. Before the fix
647+
// the advance was computed in uint8 and wrapped to 0 (then clamped to 8), so the firewall
648+
// read the transport header ~2KB too early from attacker-controlled option bytes while the
649+
// host OS parses the real header, a firewall port/proto bypass. The fix makes parseV6 land
650+
// on the same offset the host does.
651+
func Test_newPacket_v6ExtHeaderOverflow(t *testing.T) {
652+
p := &firewall.Packet{}
653+
654+
const (
655+
hdrLen = 40 // IPv6 header
656+
extLen = 2048 // (255+1)*8, the true Destination-Options header size
657+
realTCPAt = hdrLen + extLen // 2088, where the host reads the transport header
658+
forgedTCPAt = hdrLen + 8 // 48, where the pre-fix wrapped+clamped walk landed
659+
)
660+
661+
pkt := make([]byte, realTCPAt+4)
662+
pkt[0] = 0x60 // version 6
663+
pkt[6] = byte(layers.IPProtocolIPv6Destination) // NextHeader -> Destination Options
664+
pkt[40] = byte(firewall.ProtoTCP) // Dest-Options NextHeader -> TCP
665+
pkt[41] = 255 // HdrExtLen = 255
666+
667+
// Forged transport header at the pre-fix (wrong) offset: dst port 443.
668+
binary.BigEndian.PutUint16(pkt[forgedTCPAt+2:forgedTCPAt+4], 443)
669+
// Real transport header at the offset the host actually uses: dst port 22.
670+
binary.BigEndian.PutUint16(pkt[realTCPAt+2:realTCPAt+4], 22)
671+
672+
require.NoError(t, newPacket(pkt, true, p))
673+
assert.Equal(t, uint8(firewall.ProtoTCP), p.Protocol)
674+
// LocalPort is the destination port for incoming traffic. It must be the real port (22)
675+
// the host delivers to, not the forged 443 at the overflowed offset.
676+
assert.Equal(t, uint16(22), p.LocalPort, "firewall must parse the real transport header, not the overflowed offset")
677+
}

0 commit comments

Comments
 (0)