---
id: "CVE-2026-118650"
kind: "vulnerability"
aliases: ["CVE-2026-118650", "GHSA-375p-5qhx-8wq4"]
title: "Echo's middleware.Static: `%2F`-encoded path bypasses route/group middleware guarding a statically-served path"
severity: "high"
cvss: 7.5
epss: null
exploited: false
status: "open"
category: "package"
products: [{"name": "github.com/labstack/echo/v5", "fixed": ["5.4.0"], "affected": "< 5.4.0", "ecosystem": "go"}, {"name": "github.com/labstack/echo/v4", "fixed": ["4.16.0"], "affected": "< 4.16.0", "ecosystem": "go"}]
ecosystems: ["go"]
tags: ["cwe-177", "cwe-863", "echo", "go", "package", "self-hosted"]
not_affected_if: ["no-global-static-middleware", "patched-version", "static-root-public-only"]
published_at: "2026-10-28T19:54:20+00:00"
first_seen_at: "2026-10-28T19:56:29+00:00"
updated_at: "2026-10-28T19:56:37+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-375p-5qhx-8wq4", "first_seen_at": "2026-10-28T19:56:29+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-118650", "first_seen_at": "2026-10-28T20:35:10+00:00"}]
---

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

Echo's middleware.Static serves a file under a route or group guard when the request path encodes the slash as %2F, because the router and the static handler decode the path differently. An unauthenticated remote user can read files inside the static root that a route-level middleware was meant to protect, but cannot reach files outside that root.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- go:github.com/labstack/echo/v5 affected < 5.4.0 fixed 5.4.0
- go:github.com/labstack/echo/v4 affected < 4.16.0 fixed 4.16.0

## How to check

- Run `go list -m github.com/labstack/echo/v5` and `go list -m github.com/labstack/echo/v4` to see the version in use.
- Search the code for `e.Use(middleware.Static(` and check whether any directory under that root also has route or group middleware guarding it.
- Send `curl -i --path-as-is http://<host>/<guarded-dir>%2F<file>` without credentials and check for a 200 response.

## Fix

- Upgrade `github.com/labstack/echo/v5` to `5.4.0` or later.
- Upgrade `github.com/labstack/echo/v4` to `4.16.0` or later.
- Until upgraded, move files that need protection outside the Static middleware root, or serve them with `Echo#Static` behind the guard.

## Timeline

- 2026-10-28T19:56:29+00:00 ghsa https://github.com/advisories/GHSA-375p-5qhx-8wq4
- 2026-10-28T20:35:10+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-118650

## From the sources

## 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.Clean`s (~L235) and `Open`s (~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/v5@v5.3.1`, 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`):

```go
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.

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
