GHSA-jc3j-x6pg-4hmv Algernon: Host header path traversal in --domain mode reads files and runs Lua from parent dir
SummaryWhen 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):`utils/web.go` (`GetDomain`):`utils/files.go` (`URL2filename`) only sanitises the URL path — it never inspects `dirname`:`engine/flags.go` (auto-enable in CertMagic / Let's Encrypt mode):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.Host` is `..`.`GetDomain` returns `..` (no port, `net.SplitHostPort` fails — fallback path).`filepath.Join("/srv/algernon", "..")` cleans to `/srv`.`URL2filename("/srv", "/SECRET.txt")` returns `/srv/SECRET.txt`, which the handler opens with `FilePage`.For directory targets, `DirPage` lists the parent — sending `/` after `Host: ..` 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. `.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).PoCBuild the affected version:Reproduce manually:Recorded output from a real run: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.ImpactAn 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 fixReject Host header values that contain `..`, `/`, `\`, or that resolve outside the configured `serverDirOrFilename`. The simplest patch:A stronger fix when CertMagic is active is to constrain the lookup to the `certMagicDomains` allow-list that `flags.go` already builds.
CVSS v3.1 score: 8.2