GHSA-375P-5QHX-8WQ4
Vulnerability from github – Published: 2026-10-28 19:54 – Updated: 2026-10-28 19:54Summary
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.txtdoes not match a/admin/...route or group; any middleware attached to that route/group is therefore skipped. middleware.Staticselects its path source inmiddleware/static.go(~L219): with a globale.Usemount there is no wildcard route, so it usesc.Request().URL.Path, whichnet/httphas already decoded (%2F→/). It thenpath.Cleans (~L235) andOpens (~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):
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.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/labstack/echo/v5"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "5.4.0"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Go",
"name": "github.com/labstack/echo/v4"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.16.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-118650"
],
"database_specific": {
"cwe_ids": [
"CWE-177",
"CWE-863"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-28T19:54:20Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "## Summary\n\nWhen `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.\n\n**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.\n\n## Details\n\nRoot cause \u2014 the router and the static handler disagree on how to interpret `%2F`:\n\n- 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.\n- `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` \u2192 `/`). It then `path.Clean`s (~L235) and `Open`s (~L259) `admin/secret.txt`, serving the file.\n\nNet effect: for the literal path the guard runs; for the `%2F`-encoded path the guard is skipped but the same file is served.\n\nRelevant code:\n- `middleware/static.go` ~L219 (`p := c.Request().URL.Path`), ~L235 (`path.Clean`), ~L259 (`currentFS.Open`)\n- `StaticConfig.EnablePathUnescaping` docstring (`static.go` ~L60-67) \u2014 claims the default prevents the `/admin%2fprivate.txt` \u2192 `/admin/*` guard bypass; this is not true for the global mount.\n\nRelationship to prior advisories:\n- **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 \u2014 this report is that gap.\n- **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.\n\n## PoC\n\nEnvironment: `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`.\n\nMinimal app (the pattern under test: static served globally + a group guard intended to protect `/admin`):\n\n```go\npackage main\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/labstack/echo/v5/middleware\"\n)\n\nfunc main() {\n\te := echo.New()\n\te.Use(middleware.Static(\"public\")) // serves ./public globally (default filesystem)\n\n\tadmin := e.Group(\"/admin\", func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) error { return c.String(http.StatusUnauthorized, \"auth required\") }\n\t})\n\tadmin.GET(\"/secret.txt\", func(c *echo.Context) error { return c.String(http.StatusOK, \"handler\") })\n\n\te.Start(\":1323\")\n}\n```\n\nRequests (unauthenticated):\n\n```\n# Literal: route matches -\u003e group guard runs\ncurl -i http://localhost:1323/admin/secret.txt\n# =\u003e 401\n\n# %2F-encoded: route does NOT match -\u003e guard skipped, but middleware.Static serves the file\ncurl -i --path-as-is http://localhost:1323/admin%2Fsecret.txt\n# =\u003e 200, body: SECRET\n```\n\nObserved on `v5.3.1`: literal \u2192 `401`; `%2F` \u2192 `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.\n\n## Impact\n\nAn 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) \u2014 **there is no access to files outside the static root**.\n\n**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 \u2014 a statically-served directory is public by design \u2014 so maintainers may reasonably classify this as a documentation fix (correct the `EnablePathUnescaping` claim) or won\u0027t-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.\n\n## Patches\n\nFixed 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.\n\nNote: 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.\n\n## Workarounds\n\nKeep files that need protection outside the Static middleware\u0027s root directory.",
"id": "GHSA-375p-5qhx-8wq4",
"modified": "2026-10-28T19:54:20Z",
"published": "2026-10-28T19:54:20Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/labstack/echo/security/advisories/GHSA-375p-5qhx-8wq4"
},
{
"type": "WEB",
"url": "https://github.com/labstack/echo/commit/b838391e2eade87ae7e5a09d7bb5087bec271ec1"
},
{
"type": "WEB",
"url": "https://github.com/labstack/echo/commit/fc410094b2f3415cf2bf5204082137b205bfd5af"
},
{
"type": "PACKAGE",
"url": "https://github.com/labstack/echo"
},
{
"type": "WEB",
"url": "https://github.com/labstack/echo/releases/tag/v4.16.0"
},
{
"type": "WEB",
"url": "https://github.com/labstack/echo/releases/tag/v5.4.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Echo\u0027s middleware.Static: `%2F`-encoded path bypasses route/group middleware guarding a statically-served path"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.