---
id: "CVE-2026-118654"
kind: "vulnerability"
aliases: ["CVE-2026-118654", "GHSA-3pmx-cf9f-34xr"]
title: "Echo static file middleware route protection bypass via encoded dot-dot traversal"
severity: "medium"
cvss: 5.9
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-22", "echo", "go", "package", "self-hosted"]
not_affected_if: ["no-static-mounts", "protected-files-outside-mount"]
published_at: "2026-10-28T20:17:22+00:00"
first_seen_at: "2026-10-28T20:35:10+00:00"
updated_at: "2026-10-28T21:07:44+00:00"
sources: [{"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-118654", "first_seen_at": "2026-10-28T20:35:10+00:00"}, {"source": "ghsa", "url": "https://github.com/advisories/GHSA-3pmx-cf9f-34xr", "first_seen_at": "2026-10-28T21:07:44+00:00"}]
---

# Echo static file middleware route protection bypass via encoded dot-dot traversal

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.

_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

- Check the Echo version in `go.mod`; affected versions are below `4.16.0` and below `5.4.0`.
- Look for `e.Static`, `e.File` or `middleware.StaticWithConfig` mounts that serve a directory also containing files protected by route-level middleware.
- 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.
- Until upgraded, move protected files out of static filesystem mounts or apply the protection to the static mount itself.

## Timeline

- 2026-10-28T20:35:10+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-118654
- 2026-10-28T21:07:44+00:00 ghsa https://github.com/advisories/GHSA-3pmx-cf9f-34xr

## From the sources

## Summary

Echo 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.

The 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.

A 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.

## Impact

An 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.

The 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.

The `UseEscapedPathForMatching=true` variant is lower severity because it requires a non-default router configuration.

## Affected versions

Tested vulnerable:

- `github.com/labstack/echo/v5` v5.2.0
- Current `master` at `5786024ea4d85c01784796e8aa7aac53e007b39b`

No patched version is currently known.


## Policy and version checks

GitHub Private Vulnerability Reporting is enabled for `labstack/echo`. The repository has a root `SECURITY.md` and accepts security reports through GitHub Security Advisories.

The vulnerable code was revalidated on both current `master` and the latest release tag:

- Current `master`: `5786024ea4d85c01784796e8aa7aac53e007b39b`
- Latest release: `v5.2.0`, published June 14 2026
- Package: `github.com/labstack/echo/v5`

The 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.

## Reproduction

The following application shape reproduces the high-severity bypass:

```go
e := echo.New()

g := e.Group("/admin", func(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        return echo.NewHTTPError(http.StatusForbidden)
    }
})
g.GET("/*", handler)

e.StaticFS("/", fsysWithAdminSecret)
```

Control request:

```http
GET /admin/secret.txt
```

Observed result:

```text
403 Forbidden
```

Bypass request:

```http
GET /public/%2E%2E/admin/secret.txt
```

Observed result:

```text
200 OK
TOP-SECRET
```

For the non-default router variant:

```go
e := echo.New()
e.router = echo.NewRouter(echo.RouterConfig{
    UseEscapedPathForMatching: true,
})

g := e.Group("/admin", authMiddlewareReturning403)
g.GET("/*", handler)

e.StaticFS("/", fsysWithAdminSecret)
```

Bypass request:

```http
GET /public%2F..%2Fadmin%2Fsecret.txt
```

Observed result:

```text
200 OK
TOP-SECRET
```

## Proof of concept evidence

I added focused tests against the current Echo codebase. The tests exercise both `StaticDirectoryHandler` and the static middleware path.

Command:

```bash
go test -run TestHostile -v ./...
```

Observed result:

```text
=== RUN   TestHostile_StaticDirectoryHandler_EncodedSeparatorsBlocked
--- PASS: TestHostile_StaticDirectoryHandler_EncodedSeparatorsBlocked (0.00s)
=== RUN   TestHostile_StaticDirectoryHandler_EncodedDotDotBypass
--- PASS: TestHostile_StaticDirectoryHandler_EncodedDotDotBypass (0.00s)
=== RUN   TestHostile_UseEscapedPathForMatching_Bypass
--- PASS: TestHostile_UseEscapedPathForMatching_Bypass (0.00s)
PASS
ok  github.com/labstack/echo/v5  0.013s
=== RUN   TestHostile_StaticMiddleware_EncodedSeparatorsBlocked
--- PASS: TestHostile_StaticMiddleware_EncodedSeparatorsBlocked (0.01s)
=== RUN   TestHostile_StaticMiddleware_EncodedDotDotBypass
--- PASS: TestHostile_StaticMiddleware_EncodedDotDotBypass (0.00s)
=== RUN   TestHostile_StaticMiddleware_DisablePathUnescaping
--- PASS: TestHostile_StaticMiddleware_DisablePathUnescaping (0.00s)
PASS
ok  github.com/labstack/echo/v5/middleware  0.012s
```

The 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.

## Root cause

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

The vulnerable flow is:

1. The static route wildcard accepts `public/%2E%2E/admin/secret.txt`.
2. The encoded-separator guard sees no `%2F` or `%5C` and allows the request.
3. The handler unescapes the wildcard to `public/../admin/secret.txt`.
4. `path.Clean` resolves it to `admin/secret.txt`.
5. The file is served through the public static route instead of the protected `/admin/*` route.

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

## Remediation

Do 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.

Possible fixes:

1. Extend the pre-unescape guard to reject encoded dot segments such as `%2E` and `%2E%2E` in path components.
2. After unescaping, reject any wildcard where cleaning changes the path in a way that removes `.` or `..` components.
3. 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.

## Patches

Fixed 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).

## Workarounds

Put 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.

---

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