Skip to content

fix(static): disable static path unescaping by default to prevent ACL bypass (GHSA-3pmx-cf9f-34xr)#3015

Open
vishr wants to merge 1 commit into
masterfrom
fix/ghsa-3pmx-disable-static-unescape
Open

fix(static): disable static path unescaping by default to prevent ACL bypass (GHSA-3pmx-cf9f-34xr)#3015
vishr wants to merge 1 commit into
masterfrom
fix/ghsa-3pmx-disable-static-unescape

Conversation

@vishr

@vishr vishr commented Jun 15, 2026

Copy link
Copy Markdown
Member

Summary

Fixes GHSA-3pmx-cf9f-34xr, a bypass of the GHSA-vfp3-v2gw-7wfq fix (#3009).

The router matches the raw, still-encoded request path, so encoded separators and dot segments in a static wildcard are not treated as traversal during routing. Unescaping them in the static file resolver afterwards let an unauthenticated attacker read a file across a route-level middleware guard the encoded path never matched:

  • /public/%2E%2E/admin/secret.txtadmin/secret.txthigh severity, default router
  • /public%2F..%2Fadmin%2Fsecret.txt under UseEscapedPathForMatching=true, where the router decodes the path itself before the handler sees it — lower severity (non-default config)

Both returned 200 + the protected file on master at 5786024 (the exact commit the advisory tested).

Approach

Rather than keep extending an encoding denylist (the original fix blocked %2F/%5C but missed %2E%2E), this addresses the root cause: make static path unescaping opt-in. This follows @aldas's proposal (e85ee8f), rebased onto current master.

  • echo: Config.EnablePathUnescapingStaticFiles (default false) controls unescaping for Echo.Static/StaticFS and Group.Static/StaticFS.
  • middleware: StaticConfig.EnablePathUnescaping replaces the now-deprecated DisablePathUnescaping; the default is the safe, no-unescape mode.

With unescaping off, %2F/%5C/%2E%2E stay literal and never become separators or traversal.

As defense in depth — and to also close the UseEscapedPathForMatching variant, where the router (not the handler) does the decoding — any .. path segment in the resolved wildcard is rejected via pathutil.HasDotDotSegment, mirroring the fs.ValidPath "no .. element" invariant. The existing encoded-separator guard remains as a backstop on the opt-in unescaping path.

Verification

Variant master (5786024) this PR
/public/%2E%2E/admin/secret.txt (default router) 200 TOP-SECRET 404
/public%2F..%2Fadmin%2Fsecret.txt (UseEscapedPathForMatching) 200 TOP-SECRET 404
middleware equivalents (both router modes) 200 TOP-SECRET 404

Regression tests cover both variants in both router modes, the opt-in mode (encoded %20 filenames serve, but %2F/.. still rejected), and pathutil.HasDotDotSegment units. go test -race ./... and go vet ./... pass.

⚠️ Breaking change

Static files whose names contain URL-encoded characters (e.g. "hello world.txt" via /hello%20world.txt) are no longer served by default. Set EnablePathUnescapingStaticFiles (echo) / EnablePathUnescaping (middleware) to opt back in. Because this flips a default, suggest releasing as 5.3.0 with an upgrade note rather than a patch.

Notes for reviewers

  • Omitted the RawPath-preferring directory-redirect tweak from e85ee8f to keep this scoped to the vuln; happy to fold it in.
  • cc @aldas — this takes your disable-by-default approach plus the .. rejection needed to close the UseEscapedPathForMatching variant your patch didn't reach.

🤖 Generated with Claude Code

… bypass

Fixes GHSA-3pmx-cf9f-34xr, a bypass of the GHSA-vfp3-v2gw-7wfq fix.

The router matches the raw, still-encoded request path, so encoded
separators and dot segments in a static wildcard are not seen as
traversal during routing. Unescaping them in the static file resolver
afterwards let an attacker reach a file across a route-level middleware
guard the encoded path never matched:

  - /public/%2E%2E/admin/secret.txt resolved to admin/secret.txt
    (high severity, default router)
  - /public%2F..%2Fadmin%2Fsecret.txt under UseEscapedPathForMatching=true,
    where the router decodes the path itself before the handler sees it

Rather than keep extending an encoding denylist, address the root cause:
make static path unescaping opt-in.

  - echo: Config.EnablePathUnescapingStaticFiles (default false) controls
    unescaping for Echo.Static/StaticFS and Group.Static/StaticFS.
  - middleware: StaticConfig.EnablePathUnescaping replaces the now
    deprecated DisablePathUnescaping (default is the safe, no-unescape mode).

With unescaping off, %2F/%5C/%2E%2E stay literal and never become
separators or traversal. As defense in depth, and to also close the
UseEscapedPathForMatching variant (where the router, not the handler,
does the decoding), reject any ".." path segment in the resolved
wildcard via pathutil.HasDotDotSegment, mirroring the fs.ValidPath
"no .. element" invariant. The existing encoded-separator guard remains
as a backstop on the opt-in unescaping path.

BREAKING CHANGE: static files whose names contain URL-encoded characters
(e.g. "hello world.txt" via /hello%20world.txt) are no longer served by
default; set EnablePathUnescapingStaticFiles / EnablePathUnescaping to
opt back in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@aldas

aldas commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

@vishr I just created #3016

// Only '/' is a separator here: encoded backslashes are rejected earlier by
// HasEncodedPathSeparator, and on a forward-slash fs.FS a literal backslash never
// acts as a separator, so a "..\\" segment cannot traverse.
func HasDotDotSegment(p string) bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vishr , I do not think this HasDotDotSegment and any of pathutil is a maintainable solution. LLM can generate always another patch for leaking solution and complexity only grows.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Root problem here is that router and Static file serving methods and Static middleware are using path inconsistently. Creating functions to check path is just a fix for the symptom not the cause.

@vishr

vishr commented Jun 15, 2026

Copy link
Copy Markdown
Member Author

@aldas Let me take a look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants