Skip to content

fix(deps): update module github.com/labstack/echo/v4 to v5 [security] - #539

Merged
gusfcarvalho merged 6 commits into
mainfrom
renovate/major-security-gomod
Oct 29, 2026
Merged

gusfcarvalho merged 6 commits into
mainfrom
renovate/major-security-gomod

Conversation

@ccf-release-bot

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
github.com/labstack/echo/v4 v4.16.1 → v5.0.0 age confidence

Echo: Reverse Proxy Forwards Attacker-Supplied X-Real-IP to Upstream by Default

CVE-2026-118651 / GHSA-99jh-6h7p-pp36

More information

Details

Summary

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.

Affected Component
  • middleware/proxy.go — ProxyConfig.ToMiddleware(), header-rewriting block (lines 351–355)
Description
The header-preservation condition passes spoofed values to upstream

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:

  • The header is absent — req.Header.Get(echo.HeaderXRealIP) == ""; or
  • IPExtractor is explicitly configured — c.Echo().IPExtractor != nil

The 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 comment in the source code confirms intent and the security gap

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#IPExtractor explicitly, Echo fallback to legacy behavior, which is not a good choice."

Full execution chain
  1. Attacker sends GET /api/admin to the Echo proxy with header X-Real-IP: 203.0.113.1 (a trusted corporate IP)
  2. ToMiddleware() evaluates the guard at line 353: header is present (!= ""), IPExtractor is nil → condition is FALSE
  3. req.Header.Set is not called; the upstream request carries the original, attacker-supplied X-Real-IP: 203.0.113.1
  4. Upstream service checks X-Real-IP against its IP allowlist → finds 203.0.113.1 → grants access
  5. Attacker reaches an endpoint that should be restricted to the corporate IP range

Secondary impact path (audit poisoning):

  • 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.
Proof of Concept
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.

Impact
  • Upstream IP-based access controls (allowlists, blocklists, geo-restrictions) can be bypassed by supplying the expected IP in X-Real-IP
  • Upstream rate limiters that key on X-Real-IP are bypassable (same class as the Echo rate-limiter finding, now on every upstream service)
  • 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
  • Any upstream admin endpoint restricted by IP allowlist is reachable by any attacker who knows or guesses a trusted IP address
Recommended Remediation
Option 1: Always overwrite X-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
Option 2: Document and warn in Proxy() godoc

If 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 {
Credit

This vulnerability was discovered and reported by bugbunny.ai.

Patches

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.

Workarounds

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.

Severity

  • CVSS Score: 5.3 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Release Notes

labstack/echo (github.com/labstack/echo/v4)

v5.0.0

Compare Source

Echo v5 is maintenance release with major breaking changes

  • Context is now struct instead of interface and we can add method to it in the future in minor versions.
  • Adds new Router interface for possible new routing implementations.
  • Drops old logging interface and uses moderm log/slog instead.
  • Rearranges alot of methods/function signatures to make them more consistent.

Upgrade notes and v4 support:

  • Echo v4 is supported with security* updates and bug fixes until 2026-12-31
  • If you are using Echo in a production environment, it is recommended to wait until after 2026-03-31 before upgrading.
  • Until 2026-03-31, any critical issues requiring breaking v5 API changes will be addressed, even if this violates semantic versioning.

See API_CHANGES_V5.md for public API changes between v4 and v5, notes on upgrading.

Upgrading TLDR:

If you are using Linux you can migrate easier parts like that:

find . -type f -name "*.go" -exec sed -i 's/ echo.Context/ *echo.Context/g' {} +
find . -type f -name "*.go" -exec sed -i 's/echo\/v4/echo\/v5/g' {} +

macOS

find . -type f -name "*.go" -exec sed -i '' 's/ echo.Context/ *echo.Context/g' {} +
find . -type f -name "*.go" -exec sed -i '' 's/echo\/v4/echo\/v5/g' {} +

or in your favorite IDE

Replace all:

  1. echo.Context -> *echo.Context
  2. echo/v4 -> echo/v5

This should solve most of the issues. Probably the hardest part is updating all the tests.

v4.16.2

Compare Source

Fixes

  • Require a space after the case-insensitive Basic authentication scheme. Malformed schemes such as BasicX no longer invoke the credential validator. Valid credentials are still required. This adapts the v5 correction to v4's single-header API. #​3188, backport of #​3138, with credit to @​Shubham-Padkonde.
  • Reject empty cookie tokens and empty client tokens in CSRF validation. This is defense in depth; it does not introduce a new security advisory. #​3189, backport of #​3135, with credit to @​hoangtaiki.
  • Reject CSRFConfig.TokenLookup configurations that produce no recognized token extractors. Middleware creation now returns an error instead of silently skipping token validation. #​3149, thanks to @​orinnz; backport of the existing v5 validation in #​3067.

Upgrade

go get github.com/labstack/echo/[email protected]

Go 1.26 remains the module minimum. Build deployments with a supported, patched Go toolchain. No dependencies or public APIs changed in this release. Applications with invalid CSRF token lookup configuration will now fail middleware creation at startup.

The security fixes, bounded BodyDump and Decompress defaults, and dependency updates released in v4.16.1 are included unchanged. Their first patched version remains v4.16.1.

Validation: the fixes passed regression tests and upstream race-test, lint and benchmark checks on Linux, macOS and Windows with Go 1.26 and 1.27. The new regressions were also verified to fail without the fixes; local race tests and vet passed.

Full changelog: labstack/echo@v4.16.1...v4.16.2


Configuration

📅 Schedule: (in timezone UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR has been generated by Mend Renovate CLI.

@coderabbitai

coderabbitai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 1216445f-533e-4889-9c24-0bf52ce8157f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

ccf-lisa Bot and others added 3 commits October 29, 2026 05:52
Drops echo v4 entirely, which clears the osv-scan findings (all fixed
in echo v5 >= 5.4.1), and moves echo-contrib to /v5 and echo-swagger
to /v2, the releases built on echo v5.

- handlers and middleware take *echo.Context
- server and metrics start through echo.StartConfig; the API server
  shuts down gracefully on SIGINT/SIGTERM (echo v5 has no Shutdown)
- CORS: echo v5 refuses "*" with credentials, so a wildcard origin
  now allows every origin without credentials instead of panicking
- validation errors keep their {"errors": {...}} body via a
  self-marshalling status error (HTTPError.Message is now a string)
- batch Location header resolved through Router().Routes().Reverse
- tests set path values with SetPathValues; the SDK suite serves the
  API through httptest instead of polling ListenerAddr

Co-Authored-By: Claude Opus 5.5 <[email protected]>
- NewServer: an empty APIAllowedOrigins means any origin, as echo v4
  defaulted to; echo v5 panics on an empty list, which broke every
  integration suite that builds a server without origins.
- Dockerfile: build with CGO_ENABLED=0 and run on
  distroless/static-debian13. base-debian12's libssl3 carries HIGH and
  CRITICAL CVEs with no patched debian12 image yet (image-scan fails on
  main too), and the static image has no libssl or libc at all.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@ccf-lisa

ccf-lisa Bot commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Completed the echo v4 → v5 migration on top of Renovate's bump:

  • Deps: labstack/echo/v5 v5.4.2 (clears every osv-scan finding; v4 is gone from the module graph), echo-contrib/v5 v5.0.1, swaggo/echo-swagger/v2 v2.0.1.
  • Code: handlers/middleware take *echo.Context; servers start via echo.StartConfig (the API server now shuts down gracefully on SIGINT/SIGTERM); custom binder and error-handler signatures swapped; HTTPError.Message is string-only, so OSCAL validation errors use a self-marshalling status error to keep the {"errors": {...}} body; the batch Location header resolves via Router().Routes().Reverse.
  • CORS: echo v5 rejects * with credentials and an empty origin list. A * origin now allows any origin without credentials (browsers already rejected that pair), and an empty list means *, as v4 defaulted to.
  • Image: the image scan failed on base-debian12's libssl3 (on main too, and no patched debian12 digest exists yet). The binary is now built with CGO_ENABLED=0 and runs on distroless/static-debian13.
  • Tests: path params are set with SetPathValues, and the SDK suite serves through httptest.

Verified locally: full integration suite (-tags integration -p 1), unit tests, golangci-lint, make swag (no diff), and a smoke run of the production image against Postgres (health, CORS, 404 body, swagger, graceful stop).

🤖 Generated with Claude Code

@ccf-release-bot

Copy link
Copy Markdown
Contributor Author

Edited/Blocked Notification

Renovate will not automatically rebase this PR, because it does not recognize the last commit author and assumes somebody else may have edited the PR.

You can manually request rebase by checking the rebase/retry box above.

⚠️ Warning: custom changes will be lost.

Echo v5's binders return a generic 400 ("Bad Request") wrapping the
decode error, where v4 put the detail in the message. api.NewError now
reports the wrapped error, so clients see e.g. "json: cannot unmarshal
number into Go struct field loginRequest.email of type string" again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

@gusfcarvalho gusfcarvalho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ccf-review: REQUEST_CHANGES

1 Must-fix.

Comment thread internal/api/handler/oscal/response.go Resolved
ValidationErrorResponse keeps the {"errors": {field: [messages]}} 400
body that a map-valued HTTPError produced under echo v4. Assert the
status and body through echo's error handler, so a broken MarshalJSON
or StatusCode fails the test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@ccf-lisa
ccf-lisa Bot requested a review from gusfcarvalho October 29, 2026 09:49
@gusfcarvalho
gusfcarvalho merged commit bab5699 into main Oct 29, 2026
16 checks passed
@gusfcarvalho
gusfcarvalho deleted the renovate/major-security-gomod branch October 29, 2026 10:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant