DEV Community

Davi
Davi

Posted on Originally published at blog.mago.team

Server-Sent Events Security: How EventSource Breaks Your API Authentication Model

Your REST API requires a Bearer token on every request. Your WebSocket upgrade validates the token before the handshake. Your SSE endpoint accepts a persistent GET, ignores token expiry, and reconnects automatically. The connection you forgot to model was the first one attackers found.

SSE endpoints fail the authentication model that REST APIs rely on. EventSource ignores custom headers, text/event-stream bypasses CORS preflight, and auto-reconnect replays credentials after revocation. Every CVE in this pattern shares the same root cause. SSE routes are added after the auth model is built, treated as notification channels rather than data endpoints.

EventSource Cannot Send Custom Headers — This Is Spec Behavior, Not a Bug

The browser EventSource constructor accepts exactly one option: withCredentials. No headers argument exists. The WHATWG HTML Spec, section 9.2, defines this limited interface explicitly.

This pushes developers into two alternatives: cookies (via withCredentials: true) or tokens in the URL. Cookies reintroduce CSRF risk that stateless Bearer tokens eliminate. URL tokens appear in access logs, browser history, Referer headers, and CDN cache keys.

The fetch + ReadableStream workaround accepts custom headers but loses native auto-reconnect and Last-Event-ID tracking. The spec made this decision for simplicity. The API security ecosystem never updated the corresponding threat model.

text/event-stream Is a CORS Simple Request — The Browser Never Sends a Preflight

SSE is a GET with no custom headers. The browser classifies it as a CORS simple request and never sends OPTIONS. CORS enforcement becomes entirely the server's responsibility.

The Origin header is sent, but without a preflight the server's Access-Control-Allow-Origin response is the only enforcement gate. CVE-2026-46431 (Algernon, CVSS 4.3, CWE-942; GitLab Advisory Database) exposes exactly this scenario: Algernon's auto-refresh SSE server hardcoded Access-Control-Allow-Origin: * in a separate code path from the main CORS config.

Any third-party page could open a cross-origin EventSource and read the live stream via JavaScript. The attack requires no user interaction beyond visiting the malicious page. The wildcard is not a rare choice: it is the default in SSE tutorials that never mention the security context.

SSE Routes Added After Auth Middleware — The CVE Pattern

The pattern that appears consistently across advisories is the same: teams build auth correctly in REST, then add SSE routes without applying the same middleware. SSE feels like a read-only notification channel, not a data endpoint.

GHSA-9wmw-9wph-2vwp documents the Dagu case (CVE-2026-31882, CVSS 7.5). The buildStreamAuthOptions() function set BasicAuthEnabled: true but left AuthRequired: false. Thirteen SSE endpoints returned workflow data and execution logs without credentials while the REST API required authentication. The fix required explicitly setting AuthRequired: true on SSE route handlers.

GHSA-f292-66h9-fpmf documents the PraisonAI case (CVE-2026-39889, CVSS 7.5). The create_a2u_routes() function registered 5 SSE endpoints with no authentication middleware. The main gateway had been patched in the prior CVE-2026-34952; A2U was a missed code path. Those endpoints exposed agent reasoning, tool call arguments, and real-time responses without any credentials. Two different projects, the same structural mistake.

Auto-Reconnect as Credential Replay

The WHATWG spec reconnect algorithm sends an identical GET after each connection loss. The browser resends the current cookie jar state and Last-Event-ID. No credential re-evaluation occurs.

In practice: a revoked URL token keeps being resent on every reconnect attempt. The server logs 401s, but the client keeps retrying. To permanently terminate the reconnect loop, the server must respond 204 No Content. Any non-2xx response triggers exponential backoff followed by another attempt.

JWTs compound the problem. The exp claim is checked at the initial request, not during an open connection. A token with a 1-hour TTL sustains a multi-hour SSE session without any re-evaluation. Frameworks do not automatically wire token expiry to stream teardown.

SSE Event Injection via Unsanitized \r

The SSE protocol uses \r\n to delimit fields. A \r character inserted into a data or comment field without sanitization splits a single push() into two distinct browser events.

GHSA-4hxc-9384-m385 documents this pattern in the h3 npm package. The attacker controls the second event's payload entirely. This advisory is notable. It is a bypass of a prior CVE fix. The SSE wire format was treated as trusted output even after an earlier incident with the same root cause.

The mitigation is straightforward: strip \r\n from any data inserted into SSE fields before serializing. Frameworks do not apply this automatically.

What MAGO Intel Detects in API Recon

The MAGO Intel tool (intel.mago.team) identifies SSE endpoints by Content-Type: text/event-stream in the response. The auth probe tests the endpoint with no cookies and no Authorization header. A 200 response with a data stream indicates missing authentication, the exact pattern of CVE-2026-31882.

Four controls close the gap that REST auth guides never mention. Server-side Origin validation: reject connections where Origin is not in an explicit allowlist before opening the stream. Access-Control-Allow-Origin: * is never safe on SSE endpoints that return user data. Active token expiry: a server-side heartbeat validates token claims every 60 seconds; if validation fails, send an auth_expired event then close the connection. Short-lived URL tokens: single-use HMAC tokens with a 30-second TTL, bound to IP and user-agent, for contexts where cookies are unavailable. SSE-specific scanner probes: request-response scanners do not evaluate SSE correctly because the response never completes. The correct probe reads the first event frame and evaluates auth posture independently.

SSE is how your application speaks continuously to the browser. The four CVEs above were not cryptographic failures. They were missed middleware, hardcoded wildcards, and copy-paste route registration. That is the category of mistake a scanner looking for SQL injection will never catch.

Top comments (0)