VulnWatch VulnWatch
← Back to dashboard
High github · GHSA-fwqx-8365-9983

Algernon: Single-file mode unconditionally enables debug mode

Published May 19, 2026 CVSS 7.5

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:

// engine/config.go:498-502
// Make a few changes to the defaults if we are serving a single file
if ac.singleFileMode {
    ac.debugMode = true
    ac.serveJustHTTP = true
}

debugMode activates the PrettyError renderer, which on any Lua or template error response dumps:

  1. The absolute path of the file that errored (Filename field of the error template).
  2. The complete byte contents of that file, HTML-escaped, with the offending line wrapped in .
  3. 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

// engine/config.go:441-502  (inside MustServe — abridged)
switch strings.ToLower(filepath.Ext(serverFile)) {
case ".md", ".markdown":
    ...
case ".zip", ".alg":
    ...
default:
    ac.singleFileMode = true
}
// ...
// Make a few changes to the defaults if we are serving a single file
if ac.singleFileMode {
    ac.debugMode = true
    ac.serveJustHTTP = 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

// engine/config.go:393-397  (finalConfiguration, called from MustServe)
// Turn off debug mode if production mode is enabled
if ac.productionMode {
    // Turn off debug mode
    ac.debugMode = false
}

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:

1. flag parsing       -> productionMode=true, debugMode=false
2. single-file detect -> debugMode = true     (overrides production)
3. finalConfiguration -> if productionMode { debugMode = false }

On paper step 3 wins. In practice the operator-controlled execution path through MustServe for .lua files is:

1. flag parsing                                            -> productionMode=true, debugMode=false
2. single-file detect (line 493 default branch)            -> singleFileMode = true
3. if singleFileMode { debugMode = true } (line 499)       -> debugMode = true
4. if singleFileMode && ext==".lua" { singleFileMode = false; serverDir = Dir(...) }
5. ac.RunConfiguration(luaServerFilename, mux, true)       -> Lua server-conf script runs, may register handlers
6. ac.finalConfiguration(host)                              -> if productionMode { debugMode = false }   ← clamp restored

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

// engine/prettyerror.go:82-147  (abridged)
func (ac *Config) PrettyError(w http.ResponseWriter, req *http.Request, filename string, filebytes []byte, errormessage, lang string) {
    w.WriteHeader(http.StatusOK)
    w.Header().Add(contentType, htmlUTF8)
    // ... linenr parsing elided ...
    filebytes = bytes.ReplaceAll(filebytes, []byte("

Affected AI Products

adversarial
Get the weekly digest. Every Monday: top AI security stories of the week. Free.