Browse Source

IPv6FindUpperProtocol: fail closed when the extension header walk runs out of budget (#1883)

Nate Brown 4 weeks ago
parent
commit
cf3f315cd3
2 changed files with 17 additions and 3 deletions
  1. 5 3
      iputil/packet.go
  2. 12 0
      iputil/packet_test.go

+ 5 - 3
iputil/packet.go

@@ -351,8 +351,8 @@ func ipv6CreateRejectTCPPacket(packet []byte, out []byte, offset int) []byte {
 // 253/254, and real upper layer protocols like SCTP or GRE, is terminal. Walking those as extension headers
 // is a firewall bypass, so they fail closed. For a non-first fragment the returned protocol is the fragmented
 // protocol and offset points at the fragment header, there is no transport header to locate. Returns
-// ErrIPv6CouldNotFindPayload if packet is smaller than an ipv6 header or the chain is truncated before a
-// terminal protocol is reached.
+// ErrIPv6CouldNotFindPayload if packet is smaller than an ipv6 header, or the chain is truncated or longer
+// than the walk's budget before a terminal protocol is reached.
 func IPv6FindUpperProtocol(packet []byte) (nextHeader uint8, offset int, isFragment bool, anyFragment bool, err error) {
 	const maxIPv6ExtHeaders = 8
 	if len(packet) < ipv6.HeaderLen {
@@ -398,7 +398,9 @@ func IPv6FindUpperProtocol(packet []byte) (nextHeader uint8, offset int, isFragm
 			return nextHeader, offset, isFragment, anyFragment, nil
 		}
 	}
-	return nextHeader, offset, isFragment, anyFragment, nil
+	// Budget spent without reaching a terminal protocol. We don't know where the transport starts, so fail
+	// closed instead of handing back a mid-chain offset a caller would read ports from.
+	return nextHeader, offset, isFragment, anyFragment, ErrIPv6CouldNotFindPayload
 }
 
 func CreateICMPEchoResponse(packet, out []byte) []byte {

+ 12 - 0
iputil/packet_test.go

@@ -529,6 +529,17 @@ func Test_IPv6FindUpperProtocol(t *testing.T) {
 	firstFragToUDP := []byte{17, 0, 0, 1, 0, 0, 0, 1} // frag offset 0, M=1, next = UDP
 	nonFirstFrag := []byte{17, 0, 0, 9, 0, 0, 0, 1}   // frag offset non-zero, next = UDP
 	transport := []byte{0, 80, 1, 187, 0, 0, 0, 0}    // stand-in bytes, IPv6FindUpperProtocol never reads ports
+	extToHbH := []byte{0, 0, 0, 0, 0, 0, 0, 0}        // len 0 -> 8 bytes, next = Hop-by-Hop
+
+	// 8 walked headers spend the budget before the 9th slot's terminal is ever classified. This is the
+	// report 4040546 shape: without failing closed the walk hands back offset 104, and a caller reads ports
+	// from the attacker's bytes there.
+	var budgetChain []byte
+	for i := 0; i < 7; i++ {
+		budgetChain = append(budgetChain, extToHbH...)
+	}
+	budgetChain = append(budgetChain, extToTCP...)
+	budgetChain = append(budgetChain, transport...)
 
 	tests := []struct {
 		name         string
@@ -552,6 +563,7 @@ func Test_IPv6FindUpperProtocol(t *testing.T) {
 		{"truncated extension header", 0, nil, 0, ipv6.HeaderLen, false, false, ErrIPv6CouldNotFindPayload},
 		// Destination Options with a declared length (255+1)*8 = 2048 that runs past the 48 byte buffer, next = SCTP
 		{"extension length past buffer", 60, []byte{132, 255, 0, 0, 0, 0, 0, 0}, 132, ipv6.HeaderLen + 2048, false, false, ErrIPv6CouldNotFindPayload},
+		{"budget exhausted fails closed", 0, budgetChain, 0, 0, false, false, ErrIPv6CouldNotFindPayload},
 	}
 
 	for _, tt := range tests {