GitHub Advisoryadvisory · Go
GHSA-fwqx-8365-9983 Algernon: Single-file mode unconditionally enables debug mode
HIGH
Official page ↗
- Aliases
- CVE-2026-45728
- Ecosystem
- Go
- Packages
- github.com/xyproto/algernon
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
- Weaknesses
- CWE-1188, CWE-209, CWE-489, CWE-540
- Reviewed
- 2026-05-19
Affected packages
• Go github.com/xyproto/algernon: < 1.17.7 → fixed in 1.17.7
Description
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:
```go
// 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`
```go
// 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
```go
// 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
```go
// 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("= 0) && (linenr
{{.Code}}
Error message:
{{.ErrorMessage}}
```
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
```go
// engine/handlers.go (Lua handler with debugMode):
if ac.debugMode {
...
if err := ac.RunLua(recorder, req, filename, flushFunc, httpStatus); err != nil {
errortext := err.Error()
fileblock, err := ac.cache.Read(filename, ac.shouldCache(ext))
if err != nil {
fileblock = datablock.NewDa…
Related
Alias · CVE (1)
- CVE-2026-45728 Algernon is a small self-contained pure-Go web server. vulnerability