{"id":"CVE-2026-118650","kind":"vulnerability","aliases":["CVE-2026-118650","GHSA-375p-5qhx-8wq4"],"related":[],"title":"Echo's middleware.Static: `%2F`-encoded path bypasses route/group middleware guarding a statically-served path","description":"## 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 — 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` → `/`). 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) — claims the default prevents the `/admin%2fprivate.txt` → `/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 — 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 -> group guard runs\ncurl -i http://localhost:1323/admin/secret.txt\n# => 401\n\n# %2F-encoded: route does NOT match -> guard skipped, but middleware.Static serves the file\ncurl -i --path-as-is http://localhost:1323/admin%2Fsecret.txt\n# => 200, body: SECRET\n```\n\nObserved 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.\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) — **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 — 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.\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's root directory.","summary":"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.","severity":"high","cvss":7.5,"epss":null,"exploited":false,"exploited_at":null,"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"],"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.\n- Search the code for `e.Use(middleware.Static(` and check whether any directory under that root also has route or group middleware guarding it.\n- 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.\n- Upgrade `github.com/labstack/echo/v4` to `4.16.0` or later.\n- Until upgraded, move files that need protection outside the Static middleware root, or serve them with `Echo#Static` behind the guard.","status":"open","published_at":"2026-10-28T19:54:20+00:00","first_seen_at":"2026-10-28T19:56:29+00:00","triaged_at":"2026-10-28T19:56:37+00:00","updated_at":"2026-10-28T19:56:37+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-10-28T19:56:37Z","model":"claude-haiku-5-5","output":{"fix":"- Upgrade `github.com/labstack/echo/v5` to `5.4.0` or later.\n- Upgrade `github.com/labstack/echo/v4` to `4.16.0` or later.\n- Until upgraded, move files that need protection outside the Static middleware root, or serve them with `Echo#Static` behind the guard.","reach":"internet","title":"Echo: percent-encoded paths bypass route guards on static-served files","impact":"medium","summary":"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.","category":"package","nicknames":[],"who_cares":["go","echo","self-hosted"],"confidence":0.8,"needs_auth":"no","worst_case":"Unauthenticated users read files inside the static root despite route guards.","analyst_note":"","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.\n- Search the code for `e.Use(middleware.Static(` and check whether any directory under that root also has route or group middleware guarding it.\n- Send `curl -i --path-as-is http://<host>/<guarded-dir>%2F<file>` without credentials and check for a 200 response.","needs_analyst":false,"not_affected_if":["static-root-public-only","no-global-static-middleware","patched-version"],"who_is_affected":"Applications that mount middleware.Static globally with e.Use and also keep a sensitive file under the same root while relying on a route or group middleware guard to protect it. Applications that serve only public content from the static root, or that do not use middleware.Static globally, are not exposed in the same way.","user_interaction":"no","needs_special_setup":"yes","severity_if_unknown":"high"},"cost_usd":0.000891,"input_tokens":5539,"output_tokens":674,"severity_before":"high"},"analyst_status":null,"analyst_note":null,"search":"'-118650':18A,26A '-177':34B '-2026':17A,25A,161C,398C,431C,627C '-25766':432C '-55677':162C,399C,628C '-67':374C '-863':36B '/admin':274C,280C,380C,382C,500C,526C '/labstack/echo/v4':32A '/labstack/echo/v5':29A,508C '/labstack/echo/v5/middleware':511C '/labstack/echo/v5@v5.3.1':472C '/public':520C '/secret.txt':542C '1.25':462C '1323':552C '1323/admin':587C '1323/admin/secret.txt':564C '200':589C,598C '2f':4A,63B,137C,239C,260C,321C,346C,408C,567C,597C,615C '2f-encoded':566C '2fprivate.txt':381C '2fsecret.txt':275C,588C '375p':21A '401':565C,596C '404':611C '5.0.3':439C '5.2.0':406C '5qhx':22A '7wfq':167C,404C '8wq4':23A 'access':211C,225C 'access-control':224C 'addit':178C 'admin':524C 'admin.get':541C 'admin/secret.txt':331C 'advisori':396C 'alreadi':319C 'also':129C 'app':486C 'applic':113C 'attach':286C 'auth':539C 'backslash':444C 'bodi':590C 'bypass':7A,140C,189C,384C 'c':533C,544C 'c.request':314C,364C 'c.string':537C,548C 'cannot':97B 'case':198C 'caus':248C 'claim':375C 'code':360C 'configur':185C 'contain':479C 'content':602C 'contrast':604C 'control':226C 'cover':173C,424C,623C 'curl':561C,580C 'currentfs.open':369C 'cve':16A,24A,160C,397C,430C,626C 'cwe':33B,35B 'decod':71B,320C 'default':184C,377C,522C 'detail':246C 'differ':74B,441C 'disagre':255C 'disambigu':452C 'docstr':181C,371C 'e':514C 'e.group':525C 'e.start':551C 'e.staticfs':609C 'e.use':109C,304C,516C 'echo':1A,37B,43B 'echo.context':534C,545C 'echo.handlerfunc':529C,530C 'echo.new':515C 'effect':336C 'encod':5A,59B,138C,240C,270C,347C,568C 'encoding/decoding':153C 'environ':460C 'error':535C,546C 'even':642C 'exact':187C 'expect':227C 'fall':130C 'file':48B,81B,99B,145C,205C,214C,334C,356C,482C,579C,601C,638C 'filesystem':523C 'fix':155C,170C,415C,629C 'form':241C 'func':512C,527C,532C,543C 'gap':429C 'get':273C,469C 'ghsa':20A,164C,401C,434C 'ghsa-375p-5qhx-8wq4':19A 'ghsa-pgvm-wxw2-hrv9':433C 'ghsa-vfp3-v2gw-7wfq':163C,400C 'github.com':28A,31A,471C,507C,510C 'github.com/labstack/echo/v4':30A 'github.com/labstack/echo/v5':27A,506C 'github.com/labstack/echo/v5/middleware':509C 'github.com/labstack/echo/v5@v5.3.1':470C 'global':108C,196C,303C,391C,493C,521C 'global-mount':195C 'go':38B,468C,501C 'golang':461C 'group':53B,120C,283C,495C,558C 'group-level':119C 'guard':10A,54B,123C,142C,232C,342C,350C,383C,496C,559C,573C 'handler':70B,254C,412C,550C 'host':42B 'hrv9':437C 'http.statusok':549C 'http.statusunauthorized':538C 'impact':630C 'import':504C 'inaccur':192C 'inconsist':234C 'insid':82B,216C 'intend':497C 'interpret':259C 'issu':221C,442C 'l219':300C,362C 'l235':326C,366C 'l259':330C,368C 'l60':373C 'level':90B,121C 'liter':237C,339C,555C,595C 'live':215C 'local':474C 'localhost':563C,586C 'main':503C,513C 'marker':481C 'match':263C,278C,557C,572C 'meant':93B 'middlewar':9A,91B,122C,285C 'middleware.static':3A,45B,105C,110C,175C,293C,420C,517C,576C,619C,641C 'middleware/static.go':299C,361C 'minim':485C 'mismatch':154C,413C 'modul':463C 'mount':107C,197C,305C,392C 'net':335C 'net/http':317C,505C 'next':528C 'observ':592C 'offici':466C 'open':328C 'outsid':100B,206C,446C 'p':363C 'packag':39B,502C 'patch':405C,438C,608C 'path':6A,15A,58B,73B,127C,176C,202C,245C,271C,296C,340C,348C,421C,583C,620C 'path-as-i':582C 'path.clean':324C,367C 'pattern':488C 'pgvm':435C 'poc':459C 'prevent':186C,378C 'prior':395C 'protect':95B,499C 'public':478C,518C 'public/admin/secret.txt':483C 'raw':267C 'reach':98B 'read':80B 'referenc':449C 'relationship':393C 'releas':467C 'relev':359C 'reli':114C 'remot':77B,633C 'replac':475C 'report':426C,458C 'request':57B,139C,553C,616C 'requir':540C 'restrict':125C 'retriev':636C 'return':531C,536C,547C,610C 'root':85B,102B,135C,209C,219C,247C,448C,477C 'rout':51B,89B,117C,231C,264C,281C,310C,556C,569C 'route-level':88B 'route/group':8A,289C 'router':66B,250C,262C,410C 'router-vs-handl':409C 'run':343C,560C 'secret':484C,591C 'select':294C 'self':41B 'self-host':40B 'serv':14A,46B,133C,148C,213C,332C,358C,492C,519C,577C,605C,639C 'set':228C 'show':617C 'skip':292C,352C,574C 'slash':61B 'sourc':297C 'state':182C 'static':13A,69B,84B,134C,208C,218C,253C,476C,491C 'static.go':372C 'statically-serv':12A 'staticconfig.enablepathunescaping':180C,370C 'staticdirectoryhandl':158C,418C 'staticf':157C,417C 'still':147C,269C 'still-encod':268C 'summari':103C 'test':490C 'therefor':291C 'travers':203C,445C 'true':388C 'unauthent':76B,554C,632C 'url.path':315C,365C 'use':313C,464C 'user':78B,634C 'v2gw':166C,403C 'v5.3.1':594C 'vfp3':165C,402C 'via':606C 'vs':411C 'wildcard':309C 'window':443C 'wxw2':436C","nicknames":[],"mitigations":[],"who_affected":"Applications that mount middleware.Static globally with e.Use and also keep a sensitive file under the same root while relying on a route or group middleware guard to protect it. Applications that serve only public content from the static root, or that do not use middleware.Static globally, are not exposed in the same way.","tier":"B","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-28T19:54:20+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-375p-5qhx-8wq4","url":"https://github.com/advisories/GHSA-375p-5qhx-8wq4","severity":"high","published_at":"2026-10-28T19:54:20+00:00","modified_at":"2026-10-28T19:54:20+00:00","first_seen_at":"2026-10-28T19:56:29+00:00","fetched_at":"2026-10-28T19:56:29+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-118650","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-118650","severity":null,"published_at":"2026-10-28T20:17:21+00:00","modified_at":"2026-10-28T20:17:21+00:00","first_seen_at":"2026-10-28T20:35:10+00:00","fetched_at":"2026-10-28T20:35:11+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}