GHSA-9v4j-7g44-qcqw: Algernon: Auto-refresh SSE event server binds to all interfaces with Access-Control-Allow-Origin: * and no authentication
SummaryWhen auto-refresh is enabled, Algernon spins up an SSE handler that streams a `data:` line for every filesystem event under the watched directory. The handler performs **no authentication** of any kind — no shared token, no cookie check against the `permissions2` userstate, no IP allow-list, no path-prefix permission. Any client that can complete a TCP connection to the listener address receives the stream.This advisory covers the authentication gap in isolation. The cross-origin browser-reach (advisory #2b) and the network-reach (advisory #2c) amplify the impact, but each is independently fixable; this finding addresses the case where a same-origin or LAN-local client connects directly to the SSE port and reads the stream without proving anything about its identity.DetailsRoot cause — the SSE handler does not consult `permissions2` or any other authNote the handler signature: `func(w http.ResponseWriter, _ *http.Request)`. The request is discarded — no `Cookie`, `Authorization`, query-string, or remote-IP check is performed before the stream begins.In 1.17.6 the listener was placed on its own `http.ServeMux` ([recwatch/eventserver.go:200-215](../vendor/github.com/xyproto/recwatch/eventserver.go)), wholly outside the `perm.Rejected` middleware chain that gates Algernon's main HTTP listener. Even an operator who had configured admin/user path prefixes via `perm.AddAdminPath`, set a `cookieSecret`, and forced authentication on every URL of the main server had no way to gate this listener — it was unreachable from the `mux` argument the perm middleware uses.Why authentication matters for this listenerThe stream contents are not public data. They reveal:Which files the developer is actively editing, with sub-second timing precision.The existence of files inside the watched root (including files the operator may have meant to keep private — `.env.local`, `secrets.lua`, in-progress draft files).By inference, the directory layout of the project.A client that can connect to the listener obtains a low-rate continuous information disclosure for the lifetime of the connection. The handler is an infinite `for {}` loop — there is no natural session boundary or expiry.Source-level evidencePoC (against 1.17.6)No `Cookie`, no `Authorization`, no `X-Token`, no preflight, no challenge. The connection succeeds and the stream is delivered for as long as the client keeps the socket open.Impact**Confidentiality:** medium. Continuous information disclosure of filenames and edit timing to anyone who can connect.**Integrity:** none.**Availability:** low. Each connection consumes a goroutine indefinitely; many simultaneous connections can exhaust descriptors.Suggestions to fix**Primary fix — require a shared secret on the SSE endpoint.** The auto-refresh feature already injects a script into served HTML ([engine/sse.go:118-165](../engine/sse.go)); that script knows the SSE URL. Add a per-startup token, embed it in the injected JS, and require it on the SSE request:Cookie-bearing requests work too if `recwatch.EventServer` is moved behind `perm.Rejected` (see "Defence in depth"). The token approach is the smaller change.**Defence in depth — mount the SSE handler on the main mux.** Moving `recwatch.EventServerHandler` onto the main `http.ServeMux` automatically places the SSE handler behind whatever middleware the operator has configured — `perm.Rejected`, `tollbooth`, custom auth wrappers. This closes the same-origin half of the gap without a per-token implementation. Any dedicated-port path bypasses `perm.Rejected` because it uses its own `http.ServeMux`, and that path needs the token fix from "Primary fix" above.Live verificationNo `Cookie`, no `Authorization` header. Stream delivered.
CVSS v3.1: 5.3