Live › vulnerability › CVE-2026-118651
Go to dashboardThe 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.
Summary written from the sources below. Check the originals before acting.
First seen by InfraStack 2026-10-28 19:56 UTC, 1h before NVD. All times on this page are UTC.
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.
| Product | Ecosystem | Affected | Fixed in |
|---|---|---|---|
| github.com/labstack/echo/v5 | go | < 5.4.0 | 5.4.0 |
| github.com/labstack/echo/v4 | go | >= 4.0.0, <= 4.16.2 | – |
middleware.Proxy( or ProxyWithConfig( and check whether IPExtractor is set on the Echo instance.go list -m github.com/labstack/echo/v5 and go list -m github.com/labstack/echo/v4.X-Real-IP header.github.com/labstack/echo/v5 to 5.4.0.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.X-Real-IP unless it is written by the proxy.Each row is the first time that source carried this signal. The analyst’s rows are its own findings.
The 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.
middleware/proxy.go — ProxyConfig.ToMiddleware(), header-rewriting block (lines 351–355)ProxyConfig.ToMiddleware() (proxy.go:338-405) prepares forwarding headers before dispatching each request. The X-Real-IP rewrite logic at lines 351-355 reads:
// proxy.go:351-355
// 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.
if req.Header.Get(echo.HeaderXRealIP) == "" || c.Echo().IPExtractor != nil {
req.Header.Set(echo.HeaderXRealIP, c.RealIP())
}
The condition is TRUE (and therefore overwrites the header with a derived value) only when:
req.Header.Get(echo.HeaderXRealIP) == ""; orIPExtractor is explicitly configured — c.Echo().IPExtractor != nilThe 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.
An attacker who sets X-Real-IP to any value causes Echo to forward exactly that value to the upstream, with no modification or validation.
The inline comment at line 351-352 explicitly states:
"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."
The 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.
This is structurally identical to the documented insecurity in ip.go:27:
"Note: if you don't set
Echo#IPExtractorexplicitly, Echo fallback to legacy behavior, which is not a good choice."
GET /api/admin to the Echo proxy with header X-Real-IP: 203.0.113.1 (a trusted corporate IP)ToMiddleware() evaluates the guard at line 353: header is present (!= ""), IPExtractor is nil → condition is FALSEreq.Header.Set is not called; the upstream request carries the original, attacker-supplied X-Real-IP: 203.0.113.1X-Real-IP against its IP allowlist → finds 203.0.113.1 → grants accessSecondary impact path (audit poisoning):
203.0.113.1 rather than the real client IP, rendering audit trails and security analytics unreliable.package main
import (
"fmt"
"log"
"net/http"
echo "github.com/labstack/echo/v5"
"github.com/labstack/echo/v5/middleware"
)
func main() {
// Upstream server: echoes back the X-Real-IP it received
upstream := http.NewServeMux()
upstream.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "upstream sees X-Real-IP: %s\n", r.Header.Get("X-Real-IP"))
})
go http.ListenAndServe(":9000", upstream)
// Echo proxy — IPExtractor deliberately NOT set (the default)
e := echo.New()
targets := []*middleware.ProxyTarget{{URL: mustParse("http://127.0.0.1:9000")}}
e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer(targets)))
log.Fatal(e.Start(":1323"))
}
# Without spoofed header — upstream sees actual peer address
curl http://127.0.0.1:1323/
# upstream sees X-Real-IP: 127.0.0.1
# With spoofed header — upstream sees attacker-supplied value
curl -H 'X-Real-IP: 198.51.100.77' http://127.0.0.1:1323/
# upstream sees X-Real-IP: 198.51.100.77
The attacker-supplied IP 198.51.100.77 arrives at the upstream unchanged.
X-Real-IPX-Real-IP are bypassable (same class as the Echo rate-limiter finding, now on every upstream service)X-Real-IP record the attacker-chosen IP, not the real source — forensic trails are poisonedX-Real-IP from an authoritative source (preferred)Remove the conditional that preserves the incoming header. Echo, as the edge proxy, is the only authoritative source for the real client IP:
// proxy.go — always set X-Real-IP from an authoritative source
req.Header.Set(echo.HeaderXRealIP, c.RealIP())
For this to be safe, operators using Proxy() should also configure IPExtractor:
e.IPExtractor = echo.ExtractIPDirect() // no upstream proxy
// or
e.IPExtractor = echo.ExtractIPFromXFFHeader() // behind a trusted upstream LB
Proxy() godocIf backward compatibility must be preserved, add a prominent security warning to Proxy() and ProxyWithConfig():
// Proxy returns a Proxy middleware.
//
// SECURITY: By default, an incoming X-Real-IP header supplied by the client
// is forwarded to the upstream unchanged. This allows any client to spoof its
// IP identity to upstream services. Configure Echo#IPExtractor before using
// this middleware to ensure X-Real-IP is derived from a trusted source:
//
// e.IPExtractor = echo.ExtractIPDirect()
//
// See https://echo.labstack.com/guide/ip-address for details.
func Proxy(balancer ProxyBalancer) echo.MiddlewareFunc {
This vulnerability was discovered and reported by bugbunny.ai.
Fixed 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.
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.
Configure 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.
This page as .md · .json · in your agent: get_signal("CVE-2026-118651")
GitHub Advisory Database, CC-BY 4.0 · NVD data is provided by NIST and is public domain. Posts quoted belong to their authors.