Skip to content

security: /sse endpoint unauthenticated and unscoped — cross-tenant data leak risk #4393

@OneStepAt4time

Description

@OneStepAt4time

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

  1. Move to /v1/sse so the auth middleware covers it, OR delete the endpoint entirely if it duplicates /v1/events
  2. Add tenant filtering — port isGlobalEventVisibleToRequest() or accept tenant context in the bridge
  3. Add SSEConnectionLimiter — reuse existing instance from server context
  4. 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

Metadata

Metadata

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions