Security Finding — P1
Summary
The /sse SSE bridge endpoint (wired in #4373 via src/services/sse-bridge.ts) is not properly authenticated or tenant-scoped. It coexists with the properly secured /v1/events endpoint but bypasses all auth and tenant filtering.
Affected Component
Issue 1: /sse not covered by auth middleware — HIGH
The auth middleware (setupAuth in middleware/auth-setup.ts) extracts tokens for SSE routes matching:
/^\/v1\/events$|^\/v1\/sessions\/[^/]+\/(events|stream)$/
/sse does not match this pattern. It also does not start with /v1/, so:
- Bearer token is not extracted from
Authorization header
- Dashboard session cookie is not checked
- SSE token validation does not trigger
On production (auth-enabled, non-localhost): The endpoint falls through to the generic !token → 401 block. It is dead — nobody can connect. Not a data leak, but dead code shipped to production.
On localhost (zero-config, no auth): The isNoAuthLocalhost bypass applies. The endpoint is fully open with zero authentication. Any process on the machine can connect and read all events.
Issue 2: No tenant scoping — HIGH
The SSE bridge subscribes to session:* and fans out every event to every connected client:
const unsubSessions = eventBus.subscribe("session:*", (e) => {
for (const c of clients) sendSSE(c.res, e);
});
Compare with /v1/events (routes/events.ts) which uses isGlobalEventVisibleToRequest() with tenant-aware filtering. The /sse bridge has zero tenant isolation. In a multi-tenant deployment with localhost mode, all sessions across all tenants are visible to every connected client.
Issue 3: No connection limiting — MEDIUM
/v1/events uses SSEConnectionLimiter (per-IP: 10, global: 100). The /sse endpoint has no connection limiting. A single client can open unlimited SSE connections, enabling resource exhaustion.
Issue 4: Shared LocalEventBus not connected to real events — LOW
The /sse bridge creates its own new LocalEventBus() which is isolated from the actual SessionEventBus. Currently, no events flow through it — the bridge is wired but unsubscribed from real session events. This means the endpoint is functionally inert (no data leak today), but if someone wires it up, Issues 1-3 become exploitable immediately.
Recommended Fix
- Move to
/v1/sse so the auth middleware covers it, OR delete the endpoint entirely if it duplicates /v1/events
- Add tenant filtering — port
isGlobalEventVisibleToRequest() or accept tenant context in the bridge
- Add
SSEConnectionLimiter — reuse existing instance from server context
- Wire to real event source — either share the
SessionEventBus or connect to Redis Streams
Priority
P1 — while not exploitable today (dead endpoint on prod, inert event bus), the code path exists and will become a vulnerability the moment someone wires the LocalEventBus to real events.
Reporter
Themis (Security Auditor) — audit of EventBus PRs #4342, #4364, #4373
Security Finding — P1
Summary
The
/sseSSE bridge endpoint (wired in #4373 viasrc/services/sse-bridge.ts) is not properly authenticated or tenant-scoped. It coexists with the properly secured/v1/eventsendpoint but bypasses all auth and tenant filtering.Affected Component
src/services/sse-bridge.ts— SSE bridge endpointsrc/server.tsline ~717 — wiring inmain()Issue 1:
/ssenot covered by auth middleware — HIGHThe auth middleware (
setupAuthinmiddleware/auth-setup.ts) extracts tokens for SSE routes matching:/ssedoes not match this pattern. It also does not start with/v1/, so:AuthorizationheaderOn production (auth-enabled, non-localhost): The endpoint falls through to the generic
!token→ 401 block. It is dead — nobody can connect. Not a data leak, but dead code shipped to production.On localhost (zero-config, no auth): The
isNoAuthLocalhostbypass applies. The endpoint is fully open with zero authentication. Any process on the machine can connect and read all events.Issue 2: No tenant scoping — HIGH
The SSE bridge subscribes to
session:*and fans out every event to every connected client:Compare with
/v1/events(routes/events.ts) which usesisGlobalEventVisibleToRequest()with tenant-aware filtering. The/ssebridge has zero tenant isolation. In a multi-tenant deployment with localhost mode, all sessions across all tenants are visible to every connected client.Issue 3: No connection limiting — MEDIUM
/v1/eventsusesSSEConnectionLimiter(per-IP: 10, global: 100). The/sseendpoint has no connection limiting. A single client can open unlimited SSE connections, enabling resource exhaustion.Issue 4: Shared LocalEventBus not connected to real events — LOW
The
/ssebridge creates its ownnew LocalEventBus()which is isolated from the actualSessionEventBus. Currently, no events flow through it — the bridge is wired but unsubscribed from real session events. This means the endpoint is functionally inert (no data leak today), but if someone wires it up, Issues 1-3 become exploitable immediately.Recommended Fix
/v1/sseso the auth middleware covers it, OR delete the endpoint entirely if it duplicates/v1/eventsisGlobalEventVisibleToRequest()or accept tenant context in the bridgeSSEConnectionLimiter— reuse existing instance from server contextSessionEventBusor connect to Redis StreamsPriority
P1 — while not exploitable today (dead endpoint on prod, inert event bus), the code path exists and will become a vulnerability the moment someone wires the LocalEventBus to real events.
Reporter
Themis (Security Auditor) — audit of EventBus PRs #4342, #4364, #4373