highJun 25, 2026

go-chi/chi RealIP Middleware IP Spoofing via X-Forwarded-For

Shubham Kandhare
Security Engagement Manager, SecureLayer7

The chi RealIP middleware blindly trusts the leftmost value in the X-Forwarded-For header, letting any client forge their source IP and bypass access controls or corrupt audit logs.

Packagegithub.com/go-chi/chi/middleware
Ecosystemgo
Affected>= 0.9.0, <= 1.5.5
go-chi/chi RealIP Middleware IP Spoofing via X-Forwarded-For

The problem

The middleware.RealIP() function in go-chi/chi splits the X-Forwarded-For header on commas and unconditionally promotes the first (leftmost) entry into r.RemoteAddr.

Because a client controls everything to the left of what the immediate proxy appends, any caller can inject an arbitrary IP into that first position. Downstream code reading r.RemoteAddr for rate-limiting, geo-blocking, or audit logging will silently operate on the attacker-supplied value.

Proof of concept

A working proof-of-concept for this issue in github.com/go-chi/chi/middleware, with the exact payload below.

bash
# 1. Start a chi server using middleware.RealIP (see advisory PoC)
# 2. Send a request with a forged leftmost XFF entry:
curl http://localhost:8080 -H 'X-Forwarded-For: 192.0.2.2, 192.0.2.1'

# Server responds:
# remote addr: 192.0.2.2 (want 192.0.2.1)
#
# 192.0.2.2 is the attacker-forged IP.
# 192.0.2.1 is the real client IP appended by the proxy.
# RealIP picks index 0 (leftmost), so the forged IP wins.

CWE-346 (Origin Validation Error). The root cause is a hard-coded left-to-right read of XFF with no concept of which proxies are trusted. A legitimate proxy appends the connecting IP to the *right* of the existing header value, so the leftmost entry is always under client control and must never be trusted without validating all entries to its right against a known-good proxy list.

The fix in v5.3.0 (PR #967) deprecated RealIP entirely because its two design choices, mutating r.RemoteAddr and applying a one-size-fits-all header priority list, are unfixable without a breaking API change. The replacement ClientIP family requires the operator to declare exactly which trust source to use (ClientIPFromHeader, ClientIPFromXFF with explicit CIDRs, ClientIPFromXFFTrustedProxies, or ClientIPFromRemoteAddr), and the XFF variant walks right-to-left, skipping trusted entries, to find the first untrusted IP.

The fix

Upgrade to go-chi/chi/v5 >= v5.3.0 and replace middleware.RealIP with exactly one of the new ClientIPFrom* middlewares suited to your infrastructure. Read the resolved IP with middleware.GetClientIP(r) instead of r.RemoteAddr. The old RealIP function is still present but marked deprecated and remains unsafe.

Reported by convto.

References: [1][2][3]

Related research