{"id":"CVE-2026-118654","kind":"vulnerability","aliases":["CVE-2026-118654","GHSA-3pmx-cf9f-34xr"],"related":[],"title":"Echo static file middleware route protection bypass via encoded dot-dot traversal","description":"## Summary\n\nEcho v5.2.0 contains a bypass of the fix for GHSA-vfp3-v2gw-7wfq. The fix rejects encoded path separators such as `%2F` and `%5C` before static file path unescaping, but two traversal forms still bypass route-level middleware protections for static files.\n\nThe primary bypass uses encoded dot-dot segments. A request such as `/public/%2E%2E/admin/secret.txt` passes the encoded-separator guard, is unescaped to `public/../admin/secret.txt`, then `path.Clean` resolves it to `admin/secret.txt`. If `/admin/*` is protected by middleware but the static filesystem is mounted at `/`, the protected file is returned through the public static route.\n\nA second bypass exists when `UseEscapedPathForMatching=true`. In that mode, the router normalizes encoded separators before the static handler checks the wildcard. The handler receives a decoded wildcard with no `%` characters, so the encoded-separator guard does not run on the original encoded path.\n\n## Impact\n\nAn unauthenticated attacker can read files that an application intended to protect with route-level middleware, when those files are also reachable through a static filesystem mount after path normalization.\n\nThe high-severity variant works with the default router configuration and requires only encoded dot-dot path segments. The direct protected route returns 403, while the public static route returns the protected file content.\n\nThe `UseEscapedPathForMatching=true` variant is lower severity because it requires a non-default router configuration.\n\n## Affected versions\n\nTested vulnerable:\n\n- `github.com/labstack/echo/v5` v5.2.0\n- Current `master` at `5786024ea4d85c01784796e8aa7aac53e007b39b`\n\nNo patched version is currently known.\n\n\n## Policy and version checks\n\nGitHub Private Vulnerability Reporting is enabled for `labstack/echo`. The repository has a root `SECURITY.md` and accepts security reports through GitHub Security Advisories.\n\nThe vulnerable code was revalidated on both current `master` and the latest release tag:\n\n- Current `master`: `5786024ea4d85c01784796e8aa7aac53e007b39b`\n- Latest release: `v5.2.0`, published June 14 2026\n- Package: `github.com/labstack/echo/v5`\n\nThe same focused Go tests pass on the checked out `v5.2.0` / `master` commit, proving the bypass remains present in the latest released version and latest main branch at the time of reporting.\n\n## Reproduction\n\nThe following application shape reproduces the high-severity bypass:\n\n```go\ne := echo.New()\n\ng := e.Group(\"/admin\", func(next echo.HandlerFunc) echo.HandlerFunc {\n    return func(c echo.Context) error {\n        return echo.NewHTTPError(http.StatusForbidden)\n    }\n})\ng.GET(\"/*\", handler)\n\ne.StaticFS(\"/\", fsysWithAdminSecret)\n```\n\nControl request:\n\n```http\nGET /admin/secret.txt\n```\n\nObserved result:\n\n```text\n403 Forbidden\n```\n\nBypass request:\n\n```http\nGET /public/%2E%2E/admin/secret.txt\n```\n\nObserved result:\n\n```text\n200 OK\nTOP-SECRET\n```\n\nFor the non-default router variant:\n\n```go\ne := echo.New()\ne.router = echo.NewRouter(echo.RouterConfig{\n    UseEscapedPathForMatching: true,\n})\n\ng := e.Group(\"/admin\", authMiddlewareReturning403)\ng.GET(\"/*\", handler)\n\ne.StaticFS(\"/\", fsysWithAdminSecret)\n```\n\nBypass request:\n\n```http\nGET /public%2F..%2Fadmin%2Fsecret.txt\n```\n\nObserved result:\n\n```text\n200 OK\nTOP-SECRET\n```\n\n## Proof of concept evidence\n\nI added focused tests against the current Echo codebase. The tests exercise both `StaticDirectoryHandler` and the static middleware path.\n\nCommand:\n\n```bash\ngo test -run TestHostile -v ./...\n```\n\nObserved result:\n\n```text\n=== RUN   TestHostile_StaticDirectoryHandler_EncodedSeparatorsBlocked\n--- PASS: TestHostile_StaticDirectoryHandler_EncodedSeparatorsBlocked (0.00s)\n=== RUN   TestHostile_StaticDirectoryHandler_EncodedDotDotBypass\n--- PASS: TestHostile_StaticDirectoryHandler_EncodedDotDotBypass (0.00s)\n=== RUN   TestHostile_UseEscapedPathForMatching_Bypass\n--- PASS: TestHostile_UseEscapedPathForMatching_Bypass (0.00s)\nPASS\nok  github.com/labstack/echo/v5  0.013s\n=== RUN   TestHostile_StaticMiddleware_EncodedSeparatorsBlocked\n--- PASS: TestHostile_StaticMiddleware_EncodedSeparatorsBlocked (0.01s)\n=== RUN   TestHostile_StaticMiddleware_EncodedDotDotBypass\n--- PASS: TestHostile_StaticMiddleware_EncodedDotDotBypass (0.00s)\n=== RUN   TestHostile_StaticMiddleware_DisablePathUnescaping\n--- PASS: TestHostile_StaticMiddleware_DisablePathUnescaping (0.00s)\nPASS\nok  github.com/labstack/echo/v5/middleware  0.012s\n```\n\nThe control tests also confirm that the new fix blocks encoded separators such as `%2F`, `%5C`, double-encoded slash values, null bytes, and overlong slash encodings.\n\n## Root cause\n\nThe fix for GHSA-vfp3 uses a denylist-style guard that checks for encoded path separators before unescaping the static wildcard. This blocks `%2F` and `%5C`, but does not block encoded traversal segments such as `%2E%2E`.\n\nThe vulnerable flow is:\n\n1. The static route wildcard accepts `public/%2E%2E/admin/secret.txt`.\n2. The encoded-separator guard sees no `%2F` or `%5C` and allows the request.\n3. The handler unescapes the wildcard to `public/../admin/secret.txt`.\n4. `path.Clean` resolves it to `admin/secret.txt`.\n5. The file is served through the public static route instead of the protected `/admin/*` route.\n\nFor `UseEscapedPathForMatching=true`, the router decodes the path before the static handler sees the wildcard, so the handler cannot detect that encoded separators were used in the original request.\n\n## Remediation\n\nDo not rely only on detecting encoded separators. Reject encoded traversal sequences before unescaping, and reject normalized wildcard values that cross route or static mount boundaries.\n\nPossible fixes:\n\n1. Extend the pre-unescape guard to reject encoded dot segments such as `%2E` and `%2E%2E` in path components.\n2. After unescaping, reject any wildcard where cleaning changes the path in a way that removes `.` or `..` components.\n3. For `UseEscapedPathForMatching=true`, run the static-file guard against the original escaped request path before router normalization, or preserve metadata that the matched wildcard came from encoded separators.\n\n## Patches\n\nFixed in `github.com/labstack/echo/v5` v5.4.0 and `github.com/labstack/echo/v4` v4.16.0. The Static middleware and `StaticDirectoryHandler` (used by `Echo.Static`, `Echo.StaticFS`, `Group.Static` and `Group.StaticFS`) no longer serve paths with a `.`, `..` or empty segment, including after path unescaping. The encoded variant of the first vector (`/public/%2E%2E/admin/secret.txt`) was already fixed in v5.2.1 and v4.15.4 (GHSA-vfp3-v2gw-7wfq).\n\n## Workarounds\n\nPut a reverse proxy in front of the application that normalizes request paths (removes dot segments and merges slashes), or keep protected files outside the served directory.","summary":"Echo's static file handlers normalize dot segments after route matching, so a request can reach a file through a public static route even when the direct path is protected by route middleware. An unauthenticated attacker can read files the application meant to guard when those files sit under a static filesystem mount.","severity":"medium","cvss":5.9,"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-22","echo","go","package","self-hosted"],"not_affected_if":["no-static-mounts","protected-files-outside-mount"],"how_to_check":"- Check the Echo version in `go.mod`; affected versions are below `4.16.0` and below `5.4.0`.\n- Look for `e.Static`, `e.File` or `middleware.StaticWithConfig` mounts that serve a directory also containing files protected by route-level middleware.\n- Request a protected file through a path with dot segments (for example `/static/../protected/file`) and see whether it is returned.","fix":"- Upgrade Echo to `4.16.0` or `5.4.0` or later.\n- Until upgraded, move protected files out of static filesystem mounts or apply the protection to the static mount itself.","status":"open","published_at":"2026-10-28T20:17:22+00:00","first_seen_at":"2026-10-28T20:35:10+00:00","triaged_at":"2026-10-28T20:36:18+00:00","updated_at":"2026-10-28T21:07:44+00:00","category":"package","confidence":0.6,"triage":{"at":"2026-10-28T20:36:18Z","model":"claude-haiku-5-5","output":{"fix":"- Upgrade Echo to `4.16.0` or `5.4.0` or later.\n- Until upgraded, move protected files out of static filesystem mounts or apply the protection to the static mount itself.","reach":"internet","title":"Echo (Go): static file route can bypass middleware and expose protected files","impact":"medium","summary":"Echo's static file handlers normalize dot segments after route matching, so a request can reach a file through a public static route even when the direct path is protected by route middleware. An unauthenticated attacker can read files the application meant to guard when those files sit under a static filesystem mount.","category":"package","nicknames":[],"who_cares":["go","echo","self-hosted"],"confidence":0.6,"needs_auth":"no","worst_case":"Unauthenticated attacker reads protected files served through a static mount.","analyst_note":"Confirm the exact affected version ranges and the fix commit for CVE-2026-55677 and this incomplete fix via the Echo GitHub security advisory; sources only cite NVD.","how_to_check":"- Check the Echo version in `go.mod`; affected versions are below `4.16.0` and below `5.4.0`.\n- Look for `e.Static`, `e.File` or `middleware.StaticWithConfig` mounts that serve a directory also containing files protected by route-level middleware.\n- Request a protected file through a path with dot segments (for example `/static/../protected/file`) and see whether it is returned.","needs_analyst":true,"not_affected_if":["no-static-mounts","protected-files-outside-mount"],"who_is_affected":"Applications written in Go with Echo that serve files through static mounts whose directory also holds files guarded only by route-level middleware. Apps that serve no static mounts, or that keep protected files outside the mounted directory, are not exposed through this path. The sources do not give more detail on which configurations are exploitable.","user_interaction":"no","needs_special_setup":"yes","severity_if_unknown":"medium"},"cost_usd":0.000675,"input_tokens":3375,"output_tokens":675,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-118654':16A,24A '-2026':15A,23A '-22':32B '/admin':170C,441C,500C '/admin/secret.txt':462C '/labstack/echo/v4':30A '/labstack/echo/v5':27A,327C,392C,589C '/public':150C,472C,510C '0.00':563C,573C,583C '0.013':590C '14':387C '200':478C,517C '2026':388C '2e':151C,473C '2e/admin/secret.txt':152C,474C '2f':115C,511C '2fadmin':512C '2fsecret.txt':513C '34xr':21A '3pmx':19A '403':294C,466C '5786024ea4d85c01784796e8aa7aac53e007b39b':332C,381C '5c':117C '7wfq':106C 'accept':358C 'add':527C 'admin/secret.txt':168C 'advisori':364C 'affect':321C 'also':259C 'applic':79B,246C,428C 'attack':74B,240C 'authmiddlewarereturning403':501C 'bash':546C 'branch':419C 'bypass':7A,97C,128C,139C,194C,408C,435C,468C,506C,578C,582C 'c':448C 'cf9f':20A 'charact':222C 'check':211C,342C,401C 'code':367C 'codebas':534C 'command':545C 'commit':405C 'concept':524C 'configur':279C,320C 'contain':95C 'content':304C 'control':458C 'current':329C,337C,372C,379C,532C 'cve':14A,22A 'cwe':31B 'decod':218C 'default':277C,318C,487C 'direct':65B,290C 'dot':11A,12A,45B,143C,144C,285C,286C 'dot-dot':10A,142C,284C 'e':437C,491C 'e.group':440C,499C 'e.router':493C 'e.staticfs':456C,504C 'echo':1A,33B,39B,93C,533C 'echo.context':449C 'echo.handlerfunc':444C,445C 'echo.new':438C,492C 'echo.newhttperror':452C 'echo.newrouter':494C 'echo.routerconfig':495C 'enabl':348C 'encod':9A,110C,141C,156C,205C,226C,235C,283C 'encoded-separ':155C,225C 'encodeddotdotbypass':568C,572C 'encodedseparatorsblock':558C,562C 'error':450C 'even':62B 'evid':525C 'exercis':537C 'exist':195C 'file':3A,42B,56B,77B,85B,120C,136C,184C,243C,257C,303C 'filesystem':90B,178C,264C 'fix':100C,108C 'focus':395C,528C 'follow':427C 'forbidden':467C 'form':126C 'fsyswithadminsecret':457C,505C 'func':442C,447C 'g':439C,498C 'g.get':454C,502C 'get':461C,471C,509C 'ghsa':18A,103C 'ghsa-3pmx-cf9f-34xr':17A 'ghsa-vfp3-v2gw-7wfq':102C 'github':343C,362C 'github.com':26A,29A,326C,391C,588C 'github.com/labstack/echo/v4':28A 'github.com/labstack/echo/v5':25A,325C,390C,587C 'go':34B,396C,436C,490C,547C 'guard':82B,158C,228C 'handler':43B,210C,215C,455C,503C 'high':271C,433C 'high-sever':270C,432C 'host':38B 'http':460C,470C,508C 'http.statusforbidden':453C 'impact':237C 'intend':247C 'june':386C 'known':338C 'labstack/echo':350C 'latest':376C,382C,413C,417C 'level':131C,253C 'lower':310C 'main':418C 'master':330C,373C,380C,404C 'match':49B 'meant':80B 'middlewar':4A,71B,132C,174C,254C,543C 'mode':201C 'mount':91B,180C,265C 'next':443C 'non':317C,486C 'non-default':316C,485C 'normal':44B,204C,268C 'observ':463C,475C,514C,552C 'ok':479C,518C,586C 'origin':234C 'packag':35B,389C 'pass':153C,398C,559C,569C,579C,585C 'patch':334C 'path':66B,111C,121C,236C,267C,287C,544C 'path.clean':164C 'polici':339C 'present':410C 'primari':138C 'privat':344C 'proof':522C 'protect':6A,68B,133C,172C,183C,249C,291C,302C 'prove':406C 'public':59B,189C,297C 'public/../admin/secret.txt':162C 'publish':385C 'reach':54B 'reachabl':260C 'read':76B,242C 'receiv':216C 'reject':109C 'releas':377C,383C,414C 'remain':409C 'report':346C,360C,424C 'repositori':352C 'reproduc':430C 'reproduct':425C 'request':52B,147C,459C,469C,507C 'requir':281C,314C 'resolv':165C 'result':464C,476C,515C,553C 'return':186C,293C,300C,446C,451C 'revalid':369C 'root':355C 'rout':5A,48B,61B,70B,130C,191C,252C,292C,299C 'route-level':129C,251C 'router':203C,278C,319C,488C 'run':231C,549C,555C,565C,575C 'second':193C 'secret':482C,521C 'secur':359C,363C 'security.md':356C 'segment':46B,145C,288C 'self':37B 'self-host':36B 'separ':112C,157C,206C,227C 'sever':272C,311C,434C 'shape':429C 'sit':86B 'static':2A,41B,60B,89B,119C,135C,177C,190C,209C,263C,298C,542C 'staticdirectoryhandl':539C,557C,561C,567C,571C 'still':127C 'summari':92C 'tag':378C 'test':323C,397C,529C,536C,548C 'testhostil':550C,556C,560C,566C,570C,576C,580C 'text':465C,477C,516C,554C 'time':422C 'top':481C,520C 'top-secret':480C,519C 'travers':13A,125C 'true':198C,307C,497C 'two':124C 'unauthent':73B,239C 'unescap':122C,160C 'use':140C 'useescapedpathformatch':197C,306C,496C,577C,581C 'v':551C 'v2gw':105C 'v5.2.0':94C,328C,384C,403C 'variant':273C,308C,489C 'version':322C,335C,341C,415C 'vfp3':104C 'via':8A 'vulner':324C,345C,366C 'wildcard':213C,219C 'work':274C","nicknames":[],"mitigations":[],"who_affected":"Applications written in Go with Echo that serve files through static mounts whose directory also holds files guarded only by route-level middleware. Apps that serve no static mounts, or that keep protected files outside the mounted directory, are not exposed through this path. The sources do not give more detail on which configurations are exploitable.","tier":"C","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-28T20:17:22+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"nvd","external_id":"CVE-2026-118654","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-118654","severity":null,"published_at":"2026-10-28T20:17:22+00:00","modified_at":"2026-10-28T20:17:22+00:00","first_seen_at":"2026-10-28T20:35:10+00:00","fetched_at":"2026-10-28T20:35:10+00:00","note":null},{"source":"ghsa","external_id":"GHSA-3pmx-cf9f-34xr","url":"https://github.com/advisories/GHSA-3pmx-cf9f-34xr","severity":"medium","published_at":"2026-10-28T21:04:46+00:00","modified_at":"2026-10-28T21:04:47+00:00","first_seen_at":"2026-10-28T21:07:44+00:00","fetched_at":"2026-10-28T21:07:44+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"}