CVE-2026-54650: openhole-server Path Traversal via Percent-Encoded URL Segments
openhole-server forwarded URL-decoded paths to tunneled local services, letting unauthenticated attackers read files outside the web root by percent-encoding dot-segments that Go's router would…

The problem
openhole-server used r.URL.Path when forwarding requests to tunnel clients. Go's HTTP layer decodes percent-encoded sequences before populating that field, so %2e%2e becomes .. and %2f becomes / before the backend ever sees the request.
Go's ServeMux rejects literal ../ in paths, but the decoding happened before routing. Backends that resolve paths without their own canonicalization step received a fully decoded traversal sequence. An unauthenticated remote attacker could read arbitrary files outside the published web root on any tunneled local service.
Proof of concept
A working proof-of-concept for CVE-2026-54650 in github.com/bablilayoub/openhole, with the exact payload below.
GET /%2e%2e/%2e%2e/etc/passwd HTTP/1.1
Host: <openhole-server-host>
# Also works for single-level traversal:
# GET /%2e%2e/secret.txt HTTP/1.1
# Encoded slash to bypass path-based ACLs:
# GET /allowed%2fpath%2f..%2fsecret HTTP/1.1r.URL.Path is the post-decode form. Any percent-encoded dot-segment (%2e%2e) or separator (%2f) is transparently normalized to ../ or / by Go's net/http layer before the handler reads it. ServeMux only inspects the pre-decoded request target during routing, so the traversal sequences bypass the router's path-cleaning logic entirely.
The fix switches the server to forward r.URL.EscapedPath() instead, which preserves the original percent-encoded form. The CLI client was also updated to preserve the raw request-target rather than re-encoding it. Together, these changes ensure backends receive exactly what the client sent, and never a silently decoded traversal sequence.
The fix
Upgrade both openhole-server and the openhole CLI to v0.1.2. The patch commit a28c27adde2a7ed0c347b730c8707208c0f78ed3 replaces r.URL.Path with r.URL.EscapedPath() on the server side and preserves the raw request-target on the client side.
Related research
- high · 7.6OpenList Authenticated Path Traversal in Batch Rename src_name
- high · 8.2CVE-2026-55667CVE-2026-55667: File Browser Out-of-Scope File Deletion via Symlink-Following RemoveAll
- high · 7.5CVE-2026-54629CVE-2026-54629: Anyquery Local File Read via Unrestricted SQLite Virtual Table Modules
- critical · 9.1CVE-2026-50006CVE-2026-50006: Anyquery Arbitrary File Write via Unrestricted ATTACH DATABASE in Server Mode