github.com/xyproto/algernon

CVE-2026-45721

CVE-2026-45721 is a critical-severity improper input validation vulnerability in github.com/xyproto/algernon (go), affecting versions <= 1.17.6. It is fixed in 1.17.7.

Key facts
CVSS score
9.0
Critical
Attack vector
Network
Issuing authority
GitHub Advisory Database
Affected package
github.com/xyproto/algernon
Fixed in
1.17.7
Disclosed
2026

Summary

Summary When Algernon is asked for any URL path that resolves to a directory without an index file, DirPage walks upward through parent directories, past the configured server root, looking for a file named handler.lua to execute as the request handler. The loop terminates only after 100 ancestor steps or when filepath.Dir returns ., so on any absolute server-root path the search reaches the filesystem root (/ on Unix, drive letter on Windows). The first handler.lua it finds is loaded into the Lua interpreter with the full Algernon API exposed, including run3(), httpclient, os.execute, io.popen, PQ, MSSQL, raw filesystem access, and the userstate database. Any process that can write handler.lua anywhere in a parent directory of the server root obtains pre-authenticated remote code execution on the next HTTP request. This is reachable without authentication, the lookup happens before the permission check returns a hit (the perm system only gates URL prefixes, not the handler-resolution step), and any URL pointing at a directory without an index triggers the walk. On a fresh stock Algernon install the request GET / is enough. Details Root cause, unbounded upward search in DirPage dirname is the absolute path of the requested directory on disk, e.g. /srv/algernon/ when running with --prod (see engine/config.go:207). filepath.Dir("/srv/algernon") is /srv, then /, and filepath.Dir("/") returns / indefinitely. The break clause if ancestor == "." only fires for relative paths, so on every absolute server-root configuration the loop walks all the way to / and then spins on / for the remaining iterations until the 100 cap is hit. Each iteration calls ac.fs.Exists(<ancestor>/handler.lua). For the canonical --prod invocation the candidate set is: For algernon /var/www/example.com: For algernon ~/site started by user alice: The first match wins. The match is then dispatched through FilePage, which for .lua files routes to RunLua (engine/handlers.go:269) and runs the file in a pooled lua.LState with the full Algernon function map attached (engine/lua.go:30-112). Every dangerous primitive in the engine is reachable: shell-out via run3() (engine/basic.go:140-146, calling exec.Command("sh", "-c", ...)), arbitrary outbound HTTP via the httpclient module, the unsandboxed gopher-lua os/io/debug libraries, and the full permissions/userstate API. Why the request is reachable unauthenticated The permission middleware in RegisterHandlers runs before DirPage but only rejects requests whose req.URL.Path matches an admin/user prefix: Rejected returns false for / because of rootIsPublic && path == "/" (vendor/.../permissionbolt/v2/permissionbolt.go:118). Anonymous GET / therefore reaches DirPage, hits the ancestor walk, and, if any handler.lua exists anywhere in the parent chain, executes it as the response handler for /. The same applies to every directory-style URL (/foo/, /foo/bar/, …) that does not contain one of the listed index. files. Three exploit-amenable scenarios: Multi-tenant / shared hosting. Operators running multiple Algernon instances from sibling directories (/srv/tenantA, /srv/tenantB) share /srv as a common ancestor. A handler.lua placed by tenant B inside /srv becomes the catch-all handler for tenant A's requests, executing in tenant A's process with tenant A's database, redis, and filesystem permissions. The same pattern fires when a single OS user runs several algernon processes from ~/sites/<name>, anything writable at ~/sites/ (or ~/) escalates into every instance. CI runners, container images, dev workstations. A repository or container that contains any handler.lua at root, in /srv, in /var, or in /home/<user>, even one that pre-dates Algernon's installation, even one left over from a tutorial, becomes a remote-execution backdoor the moment Algernon starts. The current samples/ tree contains six handler.lua files (samples/handle/handler.lua, samples/htmx/handler.lua, etc.); copying any of them up to a parent directory by mistake is fatal. Attacker who already has unprivileged write to any parent directory (low-privileged user, world-writable /tmp if /tmp is on the parent chain, an extracted .zip/.alg web application that drops a handler.lua at the extraction root in /dev/shm or serverTempDir, etc.) gains pre-authenticated RCE for every request the Algernon process answers. The .alg extraction case is especially direct: FilePage for .alg files calls unzip.Extract(filename, webApplicationExtractionDir) with webApplicationExtractionDir = "/dev/shm" or the server temp dir (engine/handlers.go:249-266); an .alg archive containing a top-level handler.lua writes it into the extraction directory, which is itself a parent of subsequent DirPage calls for that application. Source-level evidence The Lua state pool issues states with stock library loading (no SkipOpenLibs option in lua/pool/pool.go), so the handler.lua discovered above the root has os.execute, io.popen, package.loadlib (DLL loading), debug., plus every Algernon-bound function. This is documented behaviour for trusted scripts inside the served tree; the bug is that the discovery search reaches scripts the operator never opted in to. PoC Variant A, confused-deputy via shared parent Variant B, .alg archive plants handler.lua in /dev/shm FilePage extracts .alg archives into /dev/shm (preferred) or serverTempDir. An .alg archive crafted with a top-level handler.lua lands the file into a path that is a parent of every directory served out of that extraction root. Variant C, algernon /home/<user>/site picks up ~/handler.lua Any leftover handler.lua in the user's home directory (a tutorial fragment, a copy-paste, a file from another project) is sufficient. No attacker code is needed to reproduce: copy samples/handle/handler.lua into ~/ and serve any directory under ~/. Every directory request will execute the home-directory handler. Impact Confidentiality: high, handler runs with the Algernon process's UID and reaches every database, redis instance, secret file, and cookie secret in memory. Integrity: high, handler can write to any path the process can write, including index.lua/handler.lua files of the served tree, persisting the compromise. Availability: high, handler can os.exit, hang the LState pool, or fork shell commands. Scope: changed (CVSS S:C), a write primitive against a parent directory (which the operator may consider out of scope of Algernon entirely) crosses into the Algernon process's full authority. Affected population: every Algernon deployment whose server-root path has any parent directory that is writable by a less-trusted principal, which includes (a) every --prod install on a host where any non-root user can write to /srv or /, (b) every multi-tenant deployment under a common parent, (c) every algernon <path> invocation where ~, ~/Desktop, /tmp, /var/tmp, or any other ancestor is writable by anyone other than the Algernon-process owner, (d) every server that serves .alg archives. Suggestions to fix Primary fix, clamp the walk to the server root. DirPage already has access to rootdir; the loop must terminate once ancestor ceases to be a descendant of rootdir: The 100-iteration cap and the ancestor == "." check were both attempts to bound the search; clamping to rootdir removes the underlying confused-deputy primitive instead. The same boundary check should be applied to the index. lookup loop at engine/dirhandler.go:162-168, which is currently fine because filepath.Join(dirname, indexfile) cannot escape dirname, but is worth asserting explicitly so the invariant survives future refactors. Defence in depth: Cache the resolved handler.lua path per server start and log a warning if the resolved file lives outside the server root. An operator who places handler.lua deliberately in a parent directory will see the warning and either move it or accept the risk explicitly. For .alg/zip extraction, refuse archives containing a top-level handler.lua (or rename them on extract). The extraction directory is, by design, a parent of the served tree, so a top-level handler.lua in any uploaded .alg is the same primitive. Document explicitly in TUTORIAL.md that handler.lua is searched in parent directories, current docs describe per-directory handler.lua but do not mention the upward walk. The hardening above removes the need for the warning, but the docs should track reality either way. Consider stripping the unsandboxed Lua libraries (os, io, package, debug, load/loadstring, run3) when the discovered handler lives outside the configured server root, even if the walk is otherwise permitted. The audit trail is then "Lua handler ran somewhere the operator didn't bless, but at least it couldn't shell out." Live verification (2026-05-11, Algernon 1.17.6) Reproduced against a fresh go build of xyproto/algernon@main on Windows 10. Layout: parent/handler.lua contains: Run (no admin paths configured, default permissions, no auth): Anonymous requests against / and /subdir/: The handler that lives one directory above the configured server root (poc1/parent/site/ was the path passed on the command line; poc1/parent/handler.lua is one level up and was not part of the served tree) executed in the Algernon process and its output became the HTTP 200 response body. The host's COMPUTERNAME environment variable was read via os.getenv and reflected back, proving the Lua state was unsandboxed (no SkipOpenLibs, no library stripping), os, io, package, debug are all reachable from the discovered handler. Both / and /subdir/ reproduce. / because the served root has no index. files; /subdir/ because its directory has no index. files either. The walk fires in both cases and resolves to the same handler.lua above the root. No authentication, no --debug, no special flag, no serverconf.lua. The vulnerable code path is the default flow for any directory-style request that does not find a colocated index..

Impact

What is improper input validation?

The application does not adequately validate input before processing it, allowing unexpected values to reach sensitive code paths. Typical impact: varies by context: data corruption, logic bypass, or denial of service.

Severity and exposure

CVE-2026-45721 has a CVSS score of 9.0 (Critical). The vector is network-reachable, no privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment.

A fixed version is available (1.17.7). Upgrading removes the vulnerable code path.

Affected versions

go

  • github.com/xyproto/algernon (<= 1.17.6)

Security releases

  • github.com/xyproto/algernon → 1.17.7 (go)
Kodem intelligence

Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.

Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter instead of chasing every advisory.

Kodem's runtime-powered SCA identifies whether CVE-2026-45721 is reachable in your applications. Explore open-source security for your team.

See if CVE-2026-45721 is reachable in your applications. Get a demo

Already deployed Kodem? See CVE-2026-45721 in your environment

Remediation advice

Upgrade github.com/xyproto/algernon to 1.17.7 or later to resolve this vulnerability.

Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.

Frequently asked questions about CVE-2026-45721

What is CVE-2026-45721?

CVE-2026-45721 is a critical-severity improper input validation vulnerability in github.com/xyproto/algernon (go), affecting versions <= 1.17.6. It is fixed in 1.17.7. The application does not adequately validate input before processing it, allowing unexpected values to reach sensitive code paths.

How severe is CVE-2026-45721?

CVE-2026-45721 has a CVSS score of 9.0 (Critical). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.

Which versions of github.com/xyproto/algernon are affected by CVE-2026-45721?

github.com/xyproto/algernon (go) versions <= 1.17.6 is affected.

Is there a fix for CVE-2026-45721?

Yes. CVE-2026-45721 is fixed in 1.17.7. Upgrade to this version or later.

Is CVE-2026-45721 exploitable, and should I be worried?

Whether CVE-2026-45721 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo

What actually determines whether CVE-2026-45721 is exploitable, and how bad it is?

Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.

How do I fix CVE-2026-45721?

Upgrade github.com/xyproto/algernon to 1.17.7 or later.

Stop the waste.
Protect your environment with Kodem.