github.com/xyproto/algernon

CVE-2026-45728

CVE-2026-45728 is a high-severity security vulnerability in github.com/xyproto/algernon (go), affecting versions <= 1.17.6. It is fixed in 1.17.7.

Key facts
CVSS score
7.5
High
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 invoked with a single file path instead of a directory, the documented "quick demo" workflow (algernon foo.lua, algernon page.po2, algernon index.html, algernon mywebsite.alg), singleFileMode is set to true and debugMode is forcibly enabled with no opt-out: debugMode activates the PrettyError renderer, which on any Lua or template error response dumps: The absolute path of the file that errored (Filename field of the error template). The complete byte contents of that file, HTML-escaped, with the offending line wrapped in <font style='color: red !important'>…</font>. The exception or parser error text, which in turn often quotes additional file content (Pongo2 errors include surrounding template lines; Lua tracebacks include argument values). This response is served with HTTP 200 OK to whoever sent the request that triggered the error. There is no authentication, no rate limit specific to errors, no redaction, and no opt-out short of avoiding single-file invocations entirely. Any client able to reach the server and able to provoke a runtime error in the served script obtains the full server-side source of that script and of any sibling Lua data file consulted during the request. This combines particularly badly with --prod not being effective: --prod sets productionMode = true and calls ac.debugMode = false inside finalConfiguration, but singleFileMode is computed after --prod in MustServe (line 499 vs finalConfiguration further down) and the forced debugMode = true happens before --prod's debugMode = false clamp runs, so even an operator who reasoned "I will pass --prod to be safe" gets debug-mode-on if they also pass a single Lua file. Operators routinely combine the two when running Algernon as a system unit (ExecStart=algernon --prod /etc/algernon/site.lua), unaware that single-file detection has overridden their hardening flag. Details Root cause 1, single-file detection forces debugMode = true Any single-file invocation whose extension is not .md/.zip/.alg lands in the default: branch and turns into singleFileMode = true, which then sets debugMode = true. That includes the natural quickstart inputs, .lua, .po2, .pongo2, .html, .amber, .tmpl, .jsx, .tl, .prompt, every file extension Algernon recognises as a server-renderable handler. The .lua case has a follow-up at engine/config.go:536-548 that resets singleFileMode = false so the script can read sibling files, but debugMode has already been written to true and is not unset. Root cause 2, --prod's clamp runs after the forced enable, so it is the wrong direction This clamp is in finalConfiguration. finalConfiguration is invoked from MustServe after the single-file block (MustServe line 632: ac.finalConfiguration(ac.serverHost)). So the order is: On paper step 3 wins. In practice the operator-controlled execution path through MustServe for .lua files is: Step 5 happens between the forced enable and the production clamp, and inside the configuration script Lua code may already check or expose debugMode (the debug() global is wired in [engine/serverconf.go]). Anything that latches on debugMode during step 5, including RegisterHandlers itself when called from within the server-conf script, picks up the wrong value. The clamp at step 6 may or may not retroactively fix downstream behaviour; for PrettyError, which reads ac.debugMode at request-time, the clamp does win for .lua single-file mode, but only because of the late ordering inside MustServe. For the other single-file extensions (.po2, .html, .amber, …), step 4's reset does not run, singleFileMode stays true, and --prod collides with singleFileMode semantically (a "single file" cannot meaningfully be a production system service). The forced debugMode = true survives because no later code branches re-clamp it for non-.lua paths. Empirically: algernon --prod foo.po2 (or .amber, .tmpl) on a stock Algernon binary serves PrettyError-style debug responses on template failures. --prod does not save the operator. Root cause 3, PrettyError discloses absolute path + full source The HTML template at the top of the file embeds those fields directly: Every byte of the script, including any DB connection string, API key, JWT signing secret, S3 access key, or hard-coded admin credential the operator left in index.lua for the demo, is returned to the requester. The status code is 200 OK, so caches and logs may persist the disclosure further. Root cause 4, call sites that reach PrettyError are exercised by ordinary, attacker-influenceable inputs And in PongoHandler (engine/handlers.go:81-92): The Pongo2/Amber call sites do the same for their template languages. To trigger a Lua error, an attacker needs to push the script onto a code path the developer did not test: Send a GET to an endpoint the script handles only on POST, most handle() implementations index req fields that crash on the wrong method. Submit a parameter the script tonumber()s, with a value like "abc", tonumber returns nil, and the subsequent arithmetic raises attempt to perform arithmetic on a nil value. Send a request with no Cookie header to a script that calls userstate:Username(req) and indexes the result, the resulting nil-index error returns the source. For Pongo2: send a query parameter that is referenced in a filter where the filter argument is the wrong type ({{ foo|length }} where foo is the int the script just read from req). These are not exotic conditions; they are first-five-minutes-of-fuzzing behaviour. PoC Variant A, .lua single-file invocation does not reach PrettyError Important constraint discovered during live verification: a single-file .lua invocation is routed through RunConfiguration, which registers handle() routes via engine/luahandler.go:38-58. Errors inside a handle()-registered Lua function are caught by poolL.PCall and reported through logrus.Error("Handler for "+handlePath+" failed:", err) only, they do not reach PrettyError, so a handle("/", function() error("oops") end) script does not disclose its source on the wire. The forced debugMode = true is still active for the process, and any other code path that calls PrettyError (Pongo2/Amber/Lua-file-served-from-disk) will disclose; the bare .lua single-file case alone does not. The advisory below has been narrowed accordingly, the operational exploit path is Variant B. Variant B, .po2 single-file invocation, template-side trigger page.po2: data.lua (sibling, picked up automatically by PongoHandler at engine/handlers.go:64-93): Note the disclosed file is data.lua, not the template, Pongo's variable resolution drops into Lua2funcMap, raises, and PongoHandler calls PrettyError(w, req, luafilename, luablock.Bytes(), err.Error(), "lua"). The "single-file" invocation was for page.po2, but the disclosed file is the sibling data.lua that contains the actual credentials. Variant C, --prod does not block this for non-.lua extensions The mismatch between operator intent (--prod) and runtime state (debugMode=true) is the core severity multiplier here. The flag should win; today, file-extension detection wins. Impact Confidentiality: high. Disclosure of server-side script source. In single-file demos, the disclosed file is typically the entire application, every secret, every credential, every business rule. In --prod deployments where an operator stitched together serverconf.lua + a single app.lua, the disclosed file is app.lua plus any data.lua consulted during the failing request. Integrity: none directly. Availability: none directly. Affected population: Every developer running algernon foo.lua / algernon page.po2 for a demo, evaluation, or local dev, the documented quickstart workflow. Every operator running Algernon as a system service whose ExecStart references a single Lua/Pongo/Amber file (a common pattern given that the binary is positioned as "drop-in, single-file deploy"). Every CI test job that exercises Algernon in single-file mode against attacker-controlled HTTP input (fuzz harnesses, integration tests with adversarial payloads). Suggestions to fix Primary fix, flip the default. singleFileMode should not force debugMode on; it should default it on only when --debug/-d was passed explicitly. If the developer wants the helpful error pages for the quickstart, they can pass -d (which is documented and explicit). The current behaviour is a hidden side-channel of file-extension detection. Secondary fix, let --prod win unconditionally. Hoist the production-mode clamp above the single-file detection block, so production deployments cannot have debug re-enabled by any later code path: A --prod invocation that also asks for debug should be treated as a configuration error and refused at startup with a clear log line, not silently resolved in one direction or the other. Defence in depth, narrow what PrettyError discloses even when debugMode is on. Truncate Filename to its basename (filepath.Base) so the absolute disk path of the script is not leaked; the file name alone is enough for the developer to find the file in their editor. Cap Code to ±20 lines around linenr; the developer rarely needs the full file to fix the error, and the cap meaningfully reduces secret leak when the file is large. Set Cache-Control: no-store on the response so intermediate caches and browser back-buttons do not retain it. Optionally, gate PrettyError behind a loopback / 127.0.0.1-only check when debugMode is on. A developer hitting localhost:3000 still gets the friendly error page; a remote client gets a generic 500. This matches the convention used by Rails' considerallrequests_local and Django's DEBUG = True. Documentation fix. TUTORIAL.md and the README should call out the behaviour explicitly: "algernon foo.lua enables debug-mode features that disclose your script's source on errors. Do not use single-file mode to serve real workloads; use algernon --prod /srv/algernon against a directory." Pair the doc fix with one of the code fixes above, docs alone are not enough. Live verification (2026-05-11, Algernon 1.17.6) Reproduced against a fresh go build of xyproto/algernon@main on Windows 10. Setup (Variant B, .po2 single-file): Run (no --debug, no --server, no extra hardening): Response body (excerpt, entire file is the PrettyError page): The SECRET from data.lua is rendered into the HTML response body of an unauthenticated GET /. No flag was passed to enable debug. The Contents of poc-test\poc4c\data.lua: line confirms the engine intended this as the verbose debug response, gated on ac.debugMode == true. Baseline comparison, same files served in directory mode: Empty body. The Lua parse error is logged but the source is not disclosed to the client. The difference between "leaks data.lua source verbatim" and "logs internally" is exactly the forced debugMode = true from singleFileMode. Variant A, .lua single-file does NOT trigger this code path. Verified separately: a single-file Lua script that registers handle("/", function() error("…") end) returned HTTP 200 with 0-byte body when triggered. The error was visible only in the server-process log via logrus.Error("Handler for / failed: …"). PrettyError is unreachable from handle()-registered errors; see engine/luahandler.go:38-58. The Variant A scenario was dropped from the advisory. Why .po2 doesn't get the .lua reset. The reset to singleFileMode = false at engine/config.go:547 only fires for filepath.Ext(...) == ".lua". For .po2 (and .amber, .html, .tmpl, .tl, .pongo2) the reset never runs, the forced debugMode = true persists, and PongoHandler's call to PrettyError on data-file errors disclose the source.

Impact

Severity and exposure

CVE-2026-45728 has a CVSS score of 7.5 (High). 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-45728 is reachable in your applications. Explore open-source security for your team.

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

Already deployed Kodem? See CVE-2026-45728 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-45728

What is CVE-2026-45728?

CVE-2026-45728 is a high-severity security vulnerability in github.com/xyproto/algernon (go), affecting versions <= 1.17.6. It is fixed in 1.17.7.

How severe is CVE-2026-45728?

CVE-2026-45728 has a CVSS score of 7.5 (High). 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-45728?

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

Is there a fix for CVE-2026-45728?

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

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

Whether CVE-2026-45728 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-45728 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-45728?

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

Stop the waste.
Protect your environment with Kodem.