high · 8.6CVE-2026-54650Jul 28, 2026

CVE-2026-54650: openhole-server Path Traversal via Percent-Encoded URL Segments

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

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…

Packagegithub.com/bablilayoub/openhole
Ecosystemgo
Affected<= 0.1.1
Fixed in0.1.2
CVE-2026-54650: openhole-server Path Traversal via Percent-Encoded URL Segments

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.

http
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.1

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

Reporter not attributed.

References: [1][2][3][4]

Related research