{"id":"CVE-2026-118651","kind":"vulnerability","aliases":["CVE-2026-118651","GHSA-99jh-6h7p-pp36"],"related":[],"title":"Echo: Reverse Proxy Forwards Attacker-Supplied X-Real-IP to Upstream by Default","description":"## Summary\n\nThe Echo proxy middleware (`middleware/proxy.go`) preserves an incoming `X-Real-IP` header verbatim and forwards it to the upstream service when `Echo#IPExtractor` is not configured — which is the default. Because any HTTP client can supply an arbitrary `X-Real-IP` value, an attacker can cause Echo to relay a spoofed client IP address to every upstream service behind the proxy, undermining upstream IP-based access controls, rate limiting, audit logging, and geo-restriction.\n\n## Affected Component\n\n- `middleware/proxy.go` — `ProxyConfig.ToMiddleware()`, header-rewriting block (lines 351–355)\n\n## Description\n\n### The header-preservation condition passes spoofed values to upstream\n\n`ProxyConfig.ToMiddleware()` (`proxy.go:338-405`) prepares forwarding headers before dispatching each request. The `X-Real-IP` rewrite logic at lines 351-355 reads:\n\n```go\n// proxy.go:351-355\n// Basically it's not good practice to unconditionally pass incoming x-real-ip header to upstream.\n// However, for backward compatibility, legacy behavior is preserved unless you configure Echo#IPExtractor.\nif req.Header.Get(echo.HeaderXRealIP) == \"\" || c.Echo().IPExtractor != nil {\n    req.Header.Set(echo.HeaderXRealIP, c.RealIP())\n}\n```\n\nThe condition is `TRUE` (and therefore overwrites the header with a derived value) only when:\n- The header is absent — `req.Header.Get(echo.HeaderXRealIP) == \"\"`; **or**\n- `IPExtractor` is explicitly configured — `c.Echo().IPExtractor != nil`\n\nThe condition is `FALSE` — and the header is left **unchanged** — when the request arrives with an `X-Real-IP` header already set **and** `IPExtractor` is `nil`. Because `Echo.New()` leaves `IPExtractor` as `nil`, this is the out-of-the-box behaviour for every application that uses `Proxy()` or `ProxyWithConfig()` without extra configuration.\n\nAn attacker who sets `X-Real-IP` to any value causes Echo to forward exactly that value to the upstream, with no modification or validation.\n\n### The comment in the source code confirms intent and the security gap\n\nThe inline comment at line 351-352 explicitly states:\n\n> \"Basically it's not good practice to unconditionally pass incoming x-real-ip header to upstream. However, for backward compatibility, legacy behavior is preserved unless you configure Echo#IPExtractor.\"\n\nThe framework is aware that the default is insecure and retains it for backward compatibility. No warning appears in the `Proxy()` or `ProxyWithConfig()` documentation, so operators who follow the standard setup example receive a silently insecure proxy configuration.\n\nThis is structurally identical to the documented insecurity in `ip.go:27`:\n\n> \"Note: if you don't set `Echo#IPExtractor` explicitly, Echo fallback to legacy behavior, which is not a good choice.\"\n\n### Full execution chain\n\n1. Attacker sends `GET /api/admin` to the Echo proxy with header `X-Real-IP: 203.0.113.1` (a trusted corporate IP)\n2. `ToMiddleware()` evaluates the guard at line 353: header is present (`!= \"\"`), `IPExtractor` is `nil` → condition is `FALSE`\n3. `req.Header.Set` is **not** called; the upstream request carries the original, attacker-supplied `X-Real-IP: 203.0.113.1`\n4. Upstream service checks `X-Real-IP` against its IP allowlist → finds `203.0.113.1` → grants access\n5. Attacker reaches an endpoint that should be restricted to the corporate IP range\n\nSecondary impact path (audit poisoning):\n- All upstream log entries, rate-limit counters, and geo-restriction checks are evaluated against `203.0.113.1` rather than the real client IP, rendering audit trails and security analytics unreliable.\n\n## Proof of Concept\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"log\"\n    \"net/http\"\n    echo \"github.com/labstack/echo/v5\"\n    \"github.com/labstack/echo/v5/middleware\"\n)\n\nfunc main() {\n    // Upstream server: echoes back the X-Real-IP it received\n    upstream := http.NewServeMux()\n    upstream.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) {\n        fmt.Fprintf(w, \"upstream sees X-Real-IP: %s\\n\", r.Header.Get(\"X-Real-IP\"))\n    })\n    go http.ListenAndServe(\":9000\", upstream)\n\n    // Echo proxy — IPExtractor deliberately NOT set (the default)\n    e := echo.New()\n    targets := []*middleware.ProxyTarget{{URL: mustParse(\"http://127.0.0.1:9000\")}}\n    e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer(targets)))\n    log.Fatal(e.Start(\":1323\"))\n}\n```\n\n```bash\n# Without spoofed header — upstream sees actual peer address\ncurl http://127.0.0.1:1323/\n# upstream sees X-Real-IP: 127.0.0.1\n\n# With spoofed header — upstream sees attacker-supplied value\ncurl -H 'X-Real-IP: 198.51.100.77' http://127.0.0.1:1323/\n# upstream sees X-Real-IP: 198.51.100.77\n```\n\nThe attacker-supplied IP `198.51.100.77` arrives at the upstream unchanged.\n\n## Impact\n\n- Upstream IP-based access controls (allowlists, blocklists, geo-restrictions) can be bypassed by supplying the expected IP in `X-Real-IP`\n- Upstream rate limiters that key on `X-Real-IP` are bypassable (same class as the Echo rate-limiter finding, now on every upstream service)\n- Audit logs, intrusion detection systems, and SIEM tools that rely on `X-Real-IP` record the attacker-chosen IP, not the real source — forensic trails are poisoned\n- Any upstream admin endpoint restricted by IP allowlist is reachable by any attacker who knows or guesses a trusted IP address\n\n## Recommended Remediation\n\n### Option 1: Always overwrite `X-Real-IP` from an authoritative source (preferred)\n\nRemove the conditional that preserves the incoming header. Echo, as the edge proxy, is the only authoritative source for the real client IP:\n\n```go\n// proxy.go — always set X-Real-IP from an authoritative source\nreq.Header.Set(echo.HeaderXRealIP, c.RealIP())\n```\n\nFor this to be safe, operators using `Proxy()` should also configure `IPExtractor`:\n\n```go\ne.IPExtractor = echo.ExtractIPDirect() // no upstream proxy\n// or\ne.IPExtractor = echo.ExtractIPFromXFFHeader() // behind a trusted upstream LB\n```\n\n### Option 2: Document and warn in `Proxy()` godoc\n\nIf backward compatibility must be preserved, add a prominent security warning to `Proxy()` and `ProxyWithConfig()`:\n\n```go\n// Proxy returns a Proxy middleware.\n//\n// SECURITY: By default, an incoming X-Real-IP header supplied by the client\n// is forwarded to the upstream unchanged. This allows any client to spoof its\n// IP identity to upstream services. Configure Echo#IPExtractor before using\n// this middleware to ensure X-Real-IP is derived from a trusted source:\n//\n//   e.IPExtractor = echo.ExtractIPDirect()\n//\n// See https://echo.labstack.com/guide/ip-address for details.\nfunc Proxy(balancer ProxyBalancer) echo.MiddlewareFunc {\n```\n\n## Credit\n\nThis vulnerability was discovered and reported by [bugbunny.ai](https://bugbunny.ai).\n\n## Patches\n\nFixed in `github.com/labstack/echo/v5` v5.4.0: the Proxy middleware always sets `X-Real-IP` from `Context.RealIP()`, which is the address of the direct peer unless `Echo#IPExtractor` is configured.\n\n`github.com/labstack/echo/v4` is not changed, because v4's `Context.RealIP()` without `Echo#IPExtractor` trusts `X-Forwarded-For` and `X-Real-IP` by design, and changing that default would merge all clients behind a proxy into one rate-limit bucket.\n\n## Workarounds\n\nConfigure `Echo#IPExtractor` for your deployment, for example `e.IPExtractor = echo.ExtractIPDirect()` when there is no proxy in front of the application, or `echo.ExtractIPFromXFFHeader()` / `echo.ExtractIPFromRealIPHeader()` (with `TrustIPRange` for proxies on public addresses). With an extractor set, the Proxy middleware overwrites `X-Real-IP` in v4 too.","summary":"The Echo proxy middleware passes a client's X-Real-IP header unchanged to upstream services when IPExtractor is not set, which is the default. An attacker can therefore present any IP address to upstream services that trust that header for allowlists, rate limits or audit logs.","severity":"medium","cvss":5.3,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"github.com/labstack/echo/v5","fixed":["5.4.0"],"affected":"< 5.4.0","ecosystem":"go"},{"name":"github.com/labstack/echo/v4","fixed":[],"affected":">= 4.0.0, <= 4.16.2","ecosystem":"go"}],"ecosystems":["go"],"tags":["cwe-346","echo","go","package","reverse-proxy","self-hosted"],"not_affected_if":["ipextractor-configured","no-proxy-middleware","upstream-ignores-x-real-ip"],"how_to_check":"- Search the Go code for `middleware.Proxy(` or `ProxyWithConfig(` and check whether `IPExtractor` is set on the Echo instance.\n- Check the module version with `go list -m github.com/labstack/echo/v5` and `go list -m github.com/labstack/echo/v4`.\n- Check whether an upstream service makes access decisions based on the `X-Real-IP` header.","fix":"- Upgrade `github.com/labstack/echo/v5` to `5.4.0`.\n- For `github.com/labstack/echo/v4` (no fixed version in the sources), set `e.IPExtractor = echo.ExtractIPDirect()` or `echo.ExtractIPFromXFFHeader()` on the Echo instance, which makes the Proxy middleware overwrite `X-Real-IP`.\n- Stop upstream services from trusting `X-Real-IP` unless it is written by the proxy.","status":"open","published_at":"2026-10-28T19:53:24+00:00","first_seen_at":"2026-10-28T19:56:29+00:00","triaged_at":"2026-10-28T19:56:43+00:00","updated_at":"2026-10-28T19:56:43+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-10-28T19:56:43Z","model":"claude-haiku-5-5","output":{"fix":"- Upgrade `github.com/labstack/echo/v5` to `5.4.0`.\n- For `github.com/labstack/echo/v4` (no fixed version in the sources), set `e.IPExtractor = echo.ExtractIPDirect()` or `echo.ExtractIPFromXFFHeader()` on the Echo instance, which makes the Proxy middleware overwrite `X-Real-IP`.\n- Stop upstream services from trusting `X-Real-IP` unless it is written by the proxy.","reach":"internet","title":"Echo: proxy forwards client-supplied X-Real-IP to upstream services by default","impact":"medium","summary":"The Echo proxy middleware passes a client's X-Real-IP header unchanged to upstream services when IPExtractor is not set, which is the default. An attacker can therefore present any IP address to upstream services that trust that header for allowlists, rate limits or audit logs.","category":"package","nicknames":[],"who_cares":["go","echo","reverse-proxy","self-hosted"],"confidence":0.8,"needs_auth":"no","worst_case":"Upstream services see attacker-chosen IP, bypassing IP allowlists","analyst_note":"Confirm the Echo v4 fixed version (sources say v4 is unchanged and the products field shows fixed ?) and check the GHSA page for v4 guidance.","how_to_check":"- Search the Go code for `middleware.Proxy(` or `ProxyWithConfig(` and check whether `IPExtractor` is set on the Echo instance.\n- Check the module version with `go list -m github.com/labstack/echo/v5` and `go list -m github.com/labstack/echo/v4`.\n- Check whether an upstream service makes access decisions based on the `X-Real-IP` header.","needs_analyst":true,"not_affected_if":["no-proxy-middleware","upstream-ignores-x-real-ip","ipextractor-configured"],"who_is_affected":"Applications that use Echo's Proxy middleware to forward traffic to upstream services, where those upstreams make IP-based decisions such as allowlists, rate limits, geo-restrictions or audit attribution. Applications that do not use the proxy middleware, or whose upstreams ignore X-Real-IP, are not exposed in the same way.","user_interaction":"no","needs_special_setup":"yes","severity_if_unknown":"unknown"},"cost_usd":0.001056,"input_tokens":6427,"output_tokens":827,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-118651':18A,26A '-2026':17A,25A '-346':34B '-352':383C '-355':213C,217C '-405':195C '/api/admin':491C '/labstack/echo/v4':32A '/labstack/echo/v5':29A,621C '/labstack/echo/v5/middleware':624C '1':487C '2':507C '203.0.113.1':502C,542C,556C,594C '3':524C '351':180C,212C,382C '353':514C '355':181C '4':543C '5':559C '6h7p':22A '99jh':21A 'absent':275C 'access':161C,558C 'address':77B,148C 'affect':171C 'allowlist':86B,554C 'alreadi':307C 'analyt':606C 'appear':433C 'applic':330C 'arbitrari':131C 'arriv':299C 'attack':6A,71B,138C,340C,488C,536C,560C 'attacker-suppli':5A,535C 'audit':90B,165C,576C,602C 'awar':419C 'back':630C 'backward':237C,405C,429C 'base':160C 'basic':218C,386C 'behavior':240C,408C,477C 'behaviour':327C 'behind':153C 'block':178C 'box':326C 'c.echo':251C,283C 'c.realip':256C 'call':528C 'carri':532C 'caus':140C,350C 'chain':486C 'check':546C,590C 'choic':483C 'client':50B,127C,146C,599C 'code':370C 'comment':366C,379C 'compat':238C,406C,430C 'compon':172C 'concept':610C 'condit':187C,258C,287C,521C 'configur':119C,245C,282C,338C,413C,453C 'confirm':371C 'control':162C 'corpor':505C,570C 'counter':585C 'cve':16A,24A 'cwe':33B 'default':15A,69B,123C,422C 'deriv':268C 'descript':182C 'dispatch':200C 'document':439C,460C 'echo':1A,35B,45B,94C,115C,141C,246C,351C,414C,470C,473C,494C,618C,629C 'echo.headerxrealip':250C,255C,277C 'echo.new':314C 'endpoint':563C 'entri':581C 'evalu':509C,592C 'everi':150C,329C 'exact':354C 'exampl':447C 'execut':485C 'explicit':281C,384C,472C 'extra':337C 'fallback':474C 'fals':289C,523C 'find':555C 'fmt':615C 'fmt.fprintf':646C 'follow':443C 'forward':4A,108C,197C,353C 'framework':417C 'full':484C 'func':625C,641C 'gap':376C 'geo':169C,588C 'geo-restrict':168C,587C 'get':490C 'ghsa':20A 'ghsa-99jh-6h7p-pp36':19A 'github.com':28A,31A,620C,623C 'github.com/labstack/echo/v4':30A 'github.com/labstack/echo/v5':27A,619C 'github.com/labstack/echo/v5/middleware':622C 'go':36B,215C,611C 'good':222C,390C,482C 'grant':557C 'guard':511C 'header':56B,84B,105C,176C,185C,198C,232C,265C,273C,292C,306C,400C,497C,515C 'header-preserv':184C 'header-rewrit':175C 'host':43B 'howev':235C,403C 'http':126C 'http.newservemux':639C 'http.request':645C 'http.responsewriter':643C 'ident':457C 'impact':574C 'import':614C 'incom':100C,227C,395C 'inlin':378C 'insecur':424C,451C,461C 'intent':372C 'ip':11A,55B,76B,104C,135C,147C,159C,207C,231C,305C,346C,399C,501C,506C,541C,550C,553C,571C,600C,635C,653C 'ip-bas':158C 'ip.go:27':463C 'ipextractor':62B,116C,247C,252C,279C,284C,310C,316C,415C,471C,518C 'leav':315C 'left':294C 'legaci':239C,407C,476C 'limit':88B,164C,584C 'line':179C,211C,381C,513C 'log':91B,166C,580C,616C 'logic':209C 'main':613C,626C 'middlewar':47B,96C 'middleware/proxy.go':97C,173C 'modif':362C 'net/http':617C 'nil':253C,285C,312C,318C,520C 'note':464C 'oper':441C 'origin':534C 'out-of-the-box':322C 'overwrit':263C 'packag':37B,612C 'pass':48B,188C,226C,394C 'path':575C 'poison':577C 'pp36':23A 'practic':223C,391C 'prepar':196C 'present':74B,517C 'preserv':98C,186C,242C,410C 'proof':608C 'proxi':3A,40B,46B,95C,155C,333C,436C,452C,495C 'proxy.go:338':194C 'proxy.go:351':216C 'proxyconfig.tomiddleware':174C,193C 'proxywithconfig':335C,438C 'r':644C 'rang':572C 'rate':87B,163C,583C 'rate-limit':582C 'rather':595C 'reach':561C 'read':214C 'real':10A,54B,103C,134C,206C,230C,304C,345C,398C,500C,540C,549C,598C,634C,652C 'receiv':448C,637C 'relay':143C 'render':601C 'req.header.get':249C,276C 'req.header.set':254C,525C 'request':202C,298C,531C 'restrict':170C,567C,589C 'retain':426C 'revers':2A,39B 'reverse-proxi':38B 'rewrit':177C,208C 'secondari':573C 'secur':375C,605C 'see':649C 'self':42B 'self-host':41B 'send':489C 'server':628C 'servic':60B,80B,113C,152C,545C 'set':65B,308C,342C,469C 'setup':446C 'silent':450C 'sourc':369C 'spoof':145C,189C 'standard':445C 'state':385C 'structur':456C 'summari':92C 'suppli':7A,129C,537C 'therefor':73B,262C 'tomiddlewar':508C 'trail':603C 'true':260C 'trust':82B,504C 'unchang':57B,295C 'uncondit':225C,393C 'undermin':156C 'unless':243C,411C 'unreli':607C 'upstream':13A,59B,79B,112C,151C,157C,192C,234C,359C,402C,530C,544C,579C,627C,638C,648C 'upstream.handlefunc':640C 'use':332C 'valid':364C 'valu':136C,190C,269C,349C,356C 'verbatim':106C 'w':642C,647C 'warn':432C 'without':336C 'x':9A,53B,102C,133C,205C,229C,303C,344C,397C,499C,539C,548C,633C,651C 'x-real-ip':8A,52B,101C,132C,204C,228C,302C,343C,396C,498C,538C,547C,632C,650C","nicknames":[],"mitigations":[],"who_affected":"Applications that use Echo's Proxy middleware to forward traffic to upstream services, where those upstreams make IP-based decisions such as allowlists, rate limits, geo-restrictions or audit attribution. Applications that do not use the proxy middleware, or whose upstreams ignore X-Real-IP, are not exposed in the same way.","tier":"C","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-28T19:53:24+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-99jh-6h7p-pp36","url":"https://github.com/advisories/GHSA-99jh-6h7p-pp36","severity":"medium","published_at":"2026-10-28T19:53:24+00:00","modified_at":"2026-10-28T19:53:25+00:00","first_seen_at":"2026-10-28T19:56:29+00:00","fetched_at":"2026-10-28T19:56:29+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-118651","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-118651","severity":null,"published_at":"2026-10-28T20:17:22+00:00","modified_at":"2026-10-28T20:17:22+00:00","first_seen_at":"2026-10-28T20:35:10+00:00","fetched_at":"2026-10-28T20:35:11+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"}