VYPR
High severity7.5NVD Advisory· Published Oct 28, 2026

Echo's middleware.Static: `%2F`-encoded path bypasses route/group middleware guarding a statically-served path

CVE-2026-118650

Description

Summary

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 mismatch fixed for StaticFS/StaticDirectoryHandler in CVE-2026-55677 / GHSA-vfp3-v2gw-7wfq, but the fix did not cover the middleware.Static path. In addition, the StaticConfig.EnablePathUnescaping docstring states the default configuration prevents exactly this bypass, which is inaccurate for the global-mount case.

This is not path traversal: no file outside the static root is accessed. The served file lives inside the static root; the issue is that access-control expectations set by a route guard are inconsistent between the literal and %2F-encoded forms of the same path.

Details

Root cause — the router and the static handler disagree on how to interpret %2F:

  • The router matches routes on the raw, still-encoded path, so GET /admin%2Fsecret.txt does not match a /admin/... route or group; any middleware attached to that route/group is therefore skipped.
  • middleware.Static selects its path source in middleware/static.go (~L219): with a global e.Use mount there is no wildcard route, so it uses c.Request().URL.Path, which net/http has already decoded (%2F → /). It then path.Cleans (~L235) and Opens (~L259) admin/secret.txt, serving the file.

Net effect: for the literal path the guard runs; for the %2F-encoded path the guard is skipped but the same file is served.

Relevant code: - middleware/static.go ~L219 (p := c.Request().URL.Path), ~L235 (path.Clean), ~L259 (currentFS.Open) - StaticConfig.EnablePathUnescaping docstring (static.go ~L60-67) — claims the default prevents the /admin%2fprivate.txt → /admin/* guard bypass; this is not true for the global mount.

Relationship to prior advisories: - CVE-2026-55677 / GHSA-vfp3-v2gw-7wfq (patched 5.2.0): same %2F router-vs-handler mismatch, but fixed for StaticFS/StaticDirectoryHandler. The middleware.Static path was not covered — this report is that gap. - CVE-2026-25766 / GHSA-pgvm-wxw2-hrv9 (patched 5.0.3): a *different* issue (Windows backslash traversal outside the root). Referenced only to disambiguate; not the same as this report.

PoC

Environment: golang:1.25, module using the official release (go get github.com/labstack/echo/[email protected], no local replace). Static root public/ contains a marker file public/admin/secret.txt = SECRET.

Minimal app (the pattern under test: static served globally + a group guard intended to protect /admin):

package main

import (
	"net/http"

	"github.com/labstack/echo/v5"
	"github.com/labstack/echo/v5/middleware"
)

func main() {
	e := echo.New()
	e.Use(middleware.Static("public")) // serves ./public globally (default filesystem)

	admin := e.Group("/admin", func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c *echo.Context) error { return c.String(http.StatusUnauthorized, "auth required") }
	})
	admin.GET("/secret.txt", func(c *echo.Context) error { return c.String(http.StatusOK, "handler") })

	e.Start(":1323")
}

Requests (unauthenticated):

# Literal: route matches -> group guard runs
curl -i http://localhost:1323/admin/secret.txt
# => 401

# %2F-encoded: route does NOT match -> guard skipped, but middleware.Static serves the file
curl -i --path-as-is http://localhost:1323/admin%2Fsecret.txt
# => 200, body: SECRET

Observed on v5.3.1: literal → 401; %2F → 200 with the file contents. For contrast, serving via the (patched) e.StaticFS("/") returns 404 for the same %2F request, showing the middleware.Static path was not covered by the CVE-2026-55677 fix.

Impact

An unauthenticated remote user can retrieve a file served by middleware.Static even when the application attached a route/group middleware guard to that path, by substituting / with %2F. The consequence is limited to files already inside the served static root (CWE-863 authorization inconsistency; CWE-436 interpretation conflict) — there is no access to files outside the static root.

Honest caveat on validity / severity: this only has security impact for applications that (a) serve a directory statically to the public *and* (b) place a sensitive file inside that same directory while relying on a *route-level* guard to protect it. That combination is arguably a misconfiguration — a statically-served directory is public by design — so maintainers may reasonably classify this as a documentation fix (correct the EnablePathUnescaping claim) or won't-fix rather than a code vulnerability. It is filed primarily because (1) the router-vs-handler %2F inconsistency is the exact class fixed in CVE-2026-55677 but left uncovered for middleware.Static, and (2) the docstring currently promises protection it does not provide.

Patches

Fixed in github.com/labstack/echo/v5 v5.4.0 and github.com/labstack/echo/v4 v4.16.0. With the default settings, the Static middleware resolves files from the same form of the path that the router matched, so percent-encoded paths such as /admin%2Fsecret.txt or /%61dmin/secret.txt no longer reach a file under a guarded route. Paths with ., .. or empty segments are no longer served.

Note: middleware registered with Echo#Use runs before route and group middleware, so route guards never protect files served by a global Static middleware. Keep protected files outside its root directory, or serve them with Echo#Static behind the guard.

Workarounds

Keep files that need protection outside the Static middleware's root directory.

Patches

Vulnerability mechanics

References

6

News mentions

0

No linked articles in our index yet.