Bound X-Forwarded-For scanning allocation to the hop cap (closes #133)
All checks were successful
check / check (push) Superseded by a newer commit; never tested

forwardedClientAddr now walks the header values in reverse with strings.LastIndexByte instead of joining and splitting, so allocation is bounded by the 64-hop cap rather than by header length: 1.6 MB per call becomes 16 bytes for a 1 MB chain. Semantics are unchanged, verified by differential testing against the previous implementation.
This commit was merged in pull request #136.
This commit is contained in:
2026-08-12 12:19:14 +02:00
parent fd6397154a
commit 9bfd033a29
3 changed files with 99 additions and 17 deletions

View File

@@ -81,29 +81,45 @@ func (m *Middleware) isTrustedProxy(addr netip.Addr) bool {
// Only the last maxForwardedHops entries are examined. A longer chain
// is padding, and running out of hops falls back to the peer address
// the same way an unreadable hop does.
//
// The entries are cut off the right end of each header value in place
// rather than split out of it: the receiver is unauthenticated and a
// client can pad the header up to MaxHeaderBytes, so splitting would
// allocate in proportion to the padding (about 8 MB for a 1 MB
// header) before the cap could discard any of it. Multiple header
// values are walked in reverse for the same reason, since joining
// them copies the whole chain.
func (m *Middleware) forwardedClientAddr(
r *http.Request,
) (netip.Addr, bool) {
hops := strings.Split(
strings.Join(r.Header.Values("X-Forwarded-For"), ","), ",",
)
if len(hops) > maxForwardedHops {
hops = hops[len(hops)-maxForwardedHops:]
}
seen := 0
for _, hop := range slices.Backward(hops) {
hop = strings.TrimSpace(hop)
if hop == "" {
continue
}
for _, value := range slices.Backward(
r.Header.Values("X-Forwarded-For"),
) {
for last := false; !last && seen < maxForwardedHops; seen++ {
hop := value
addr, err := netip.ParseAddr(hop)
if err != nil {
return netip.Addr{}, false
}
comma := strings.LastIndexByte(value, ',')
if comma < 0 {
last = true
} else {
hop, value = value[comma+1:], value[:comma]
}
if addr = normalizeAddr(addr); !m.isTrustedProxy(addr) {
return addr, true
hop = strings.TrimSpace(hop)
if hop == "" {
continue
}
addr, err := netip.ParseAddr(hop)
if err != nil {
return netip.Addr{}, false
}
if addr = normalizeAddr(addr); !m.isTrustedProxy(addr) {
return addr, true
}
}
}