Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐹
🐹 Go
Not in CISA KEV
MEDIUM severity

GHSA-9v4j-7g44-qcqw — algernon

MEDIUM

GHSA-9v4j-7g44-qcqw is a medium-severity (CVSS 5.3) vulnerability in github.com/xyproto/algernon. A fix is available for github.com/xyproto/algernon — see the affected versions and patch details below.

Algernon: Auto-refresh SSE event server binds to all interfaces with Access-Control-Allow-Origin: * and no authentication

Also known asGO-2026-5300
Published
May 19, 2026
Updated
Jun 25, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Jun 25, 2026 · OSV.dev, FIRST.org (EPSS)

Real-World Exposure

1 pkg affected
🐹github.com/xyproto/algernon

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.

Description

Summary

When 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.

Details

Root cause — the SSE handler does not consult permissions2 or any other auth

// vendor/github.com/xyproto/recwatch/eventserver.go:100-144  (1.17.6)
func GenFileChangeEvents(events TimeEventMap, mut *sync.Mutex, maxAge time.Duration, allowed string) http.HandlerFunc {
    return func(w http.ResponseWriter, _ *http.Request) {
        w.Header().Set("Content-Type", "text/event-stream;charset=utf-8")
        w.Header().Set("Cache-Control", "no-cache")
        w.Header().Set("Connection", "keep-alive")
        w.Header().Set("Access-Control-Allow-Origin", allowed)
        // ... loop emits one SSE record per filename touched ...
    }
}

Note 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), 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 listener

The 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 evidence

$ rg -n 'GenFileChangeEvents|EventServer\(' vendor/github.com/xyproto/recwatch/
vendor/github.com/xyproto/recwatch/eventserver.go:101:func GenFileChangeEvents(events TimeEventMap, mut *sync.Mutex, maxAge time.Duration, allowed string) http.HandlerFunc {
vendor/github.com/xyproto/recwatch/eventserver.go:177:func EventServer(path, allowed, eventAddr, eventPath string, refreshDuration time.Duration) {

$ rg -n 'Cookie|Authorization|Token|state\.User' vendor/github.com/xyproto/recwatch/eventserver.go
# zero matches — no authentication primitive is referenced anywhere in the file

PoC (against 1.17.6)

# 1. Operator runs algernon with auto-refresh on a project directory:
algernon -a /path/to/project   # spins up :5553 on Linux/macOS, localhost:5553 on Windows

# 2. Any client that can reach the listener connects without credentials:
curl -sN http://<server>:5553/sse
# => id: 0
#    data: /path/to/project/secret-notes.md
#
#    id: 1
#    data: /path/to/project/.env.local

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); that script knows the SSE URL. Add a per-startup token, embed it in the injected JS, and require it on the SSE request:

// engine/sse.go -- in InsertAutoRefresh
tmplData.SessionToken = ac.sseToken    // generated once at startup, e.g. crypto/rand 32 bytes

// JS:
//   var source = new EventSource('...?token={{.SessionToken}}');

// recwatch handler:
//   if subtle.ConstantTimeCompare([]byte(r.URL.Query().Get("token")),
//                                 []byte(serverToken)) != 1 {
//       http.Error(w, "forbidden", http.StatusForbidden); return
//   }

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 verification

$ ./algernon.exe --nodb --httponly --server -a --addr 127.0.0.1:18781 --quiet poc2/site
$ ( curl -sN --max-time 4 http://127.0.0.1:5553/sse > stream.txt &
    sleep 1
    echo "edit-1" >> poc2/site/secret-notes.md
    echo "edit-2" >> poc2/site/.env.local
    wait )
$ cat stream.txt
id: 0
data: C:\Users\xbox\Desktop\VulnTesting\algernon-main\poc-test\poc2\site\secret-notes.md

id: 1
data: C:\Users\xbox\Desktop\VulnTesting\algernon-main\poc-test\poc2\site\.env.local

No Cookie, no Authorization header. Stream delivered.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐹Gogithub.com/xyproto/algernonall versions1.17.7go get github.com/xyproto/algernon@v1.17.7

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/xyproto/algernon, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update github.com/xyproto/algernon to 1.17.7 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-9v4j-7g44-qcqw is resolved across your whole dependency graph.

  3. Workarounds

    Assume what was exposed is already known: rotate any credential, token or key that the affected component could return, restrict the endpoint to callers that genuinely need it, and strip sensitive fields from responses and error output at the boundary rather than relying on the client not to read them.

Frequently Asked Questions

### Summary When 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 in
O3 Security · Impact-Aware SCA

Is GHSA-9v4j-7g44-qcqw in your dependencies?

Find it across Go, including transitive dependencies.

GHSA-9v4j-7g44-qcqw: algernon (Medium 5.3) | O3 Security