Skip to content

fix(dashboard): drop wildcard CORS on authenticated SSE endpoint#4

Open
jackthepunished wants to merge 1 commit intomainfrom
fix/issue-347-sse-cors
Open

fix(dashboard): drop wildcard CORS on authenticated SSE endpoint#4
jackthepunished wants to merge 1 commit intomainfrom
fix/issue-347-sse-cors

Conversation

@jackthepunished
Copy link
Copy Markdown
Owner

Summary

  • Fixes Dashboard SSE endpoint uses wildcard Access-Control-Allow-Origin poyrazK/thecloud#347. /api/dashboard/stream (an authenticated SSE endpoint) was sending Access-Control-Allow-Origin: *, letting any third-party origin in a logged-in user's browser subscribe to that user's real-time dashboard events.
  • The handler now takes authoritative control of its CORS headers and validates the request Origin against an allowlist (sourced from the existing WS_ALLOWED_ORIGINS env var that already gates the dashboard's WebSocket upgrade).
  • Behaviour:
    • empty Origin (same-origin / non-browser): no Access-Control-* headers emitted
    • Origin in allowlist: echoed back with Vary: Origin and Allow-Credentials: true
    • * allowlist entry: echoes the request Origin (never literal *, which EventSource rejects with credentials)
    • otherwise: 403

Test plan

  • New TestDashboardHandlerStreamEventsCORS covers: allowed origin echoed, disallowed origin → 403 (service not called), empty allowlist rejects cross-origin, * echoes the origin, same-origin emits no CORS header.
  • Existing TestDashboardHandlerStreamEvents tightened to assert no wildcard leaks on same-origin requests.
  • go test ./internal/handlers/... — please run; Go isn't available in this dev container so I couldn't execute it locally.
  • Verify production wiring sets WS_ALLOWED_ORIGINS to the dashboard origin(s); without it the SSE stream becomes same-origin only.

The /api/dashboard/stream SSE handler hardcoded
Access-Control-Allow-Origin: *, allowing any third-party origin in a
logged-in user's browser to subscribe to that user's real-time dashboard
events.

Replace with an allowlist sourced from WS_ALLOWED_ORIGINS (same
dashboard frontend already protects the WebSocket upgrade). The handler
now takes authoritative control of CORS for this route, clearing the
permissive defaults set by httputil.CORS() upstream:

  - empty Origin (same-origin / non-browser): no CORS headers emitted
  - Origin in allowlist: echoed back with Vary: Origin and credentials
  - "*" entry: echoes the request Origin (never literal "*", which
    EventSource rejects with credentials)
  - otherwise: 403

Fixes poyrazK#347.
Copilot AI review requested due to automatic review settings April 29, 2026 20:19
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 29, 2026

Warning

Rate limit exceeded

@jackthepunished has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 58 minutes and 44 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 25011a1e-ee8c-4def-bcf4-b69f9f3d9b53

📥 Commits

Reviewing files that changed from the base of the PR and between e2cc272 and f56de60.

📒 Files selected for processing (3)
  • internal/api/setup/router.go
  • internal/handlers/dashboard_handler.go
  • internal/handlers/dashboard_handler_test.go
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-347-sse-cors

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 58 minutes and 44 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Hardens the authenticated dashboard SSE stream (/api/dashboard/stream) against cross-origin subscription by removing the wildcard CORS behavior and enforcing an explicit Origin allowlist (aligned with the existing WebSocket allowlist configuration).

Changes:

  • Adds per-endpoint CORS enforcement for the dashboard SSE stream, removing Access-Control-Allow-Origin: * and validating Origin against an allowlist.
  • Extends the dashboard handler constructor to accept and parse allowed origins (including comma-separated input).
  • Adds/updates tests to assert no wildcard CORS leakage and to validate allow/deny CORS behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
internal/handlers/dashboard_handler.go Implements allowlist-based CORS handling for SSE and removes wildcard header emission.
internal/handlers/dashboard_handler_test.go Tightens existing SSE test and adds a new CORS-focused test suite for the stream endpoint.
internal/api/setup/router.go Wires cfg.WSAllowedOrigins into the dashboard handler so SSE and WS share the same allowlist source.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 148 to +156
go r.ServeHTTP(w, req)

time.Sleep(100 * time.Millisecond)
cancel()

assert.Contains(t, w.Header().Get("Content-Type"), "text/event-stream")
assert.Contains(t, w.Body.String(), "event:summary")
// Same-origin (no Origin header) → must not emit a wildcard CORS header. See #347.
assert.Empty(t, w.Header().Get("Access-Control-Allow-Origin"))
runStream := func(t *testing.T, h *DashboardHandler, origin string) *httptest.ResponseRecorder {
t.Helper()
gin.SetMode(gin.TestMode)
r := gin.New()
Comment on lines +194 to +197
headers.Set("Access-Control-Allow-Origin", origin)
headers.Set("Access-Control-Allow-Credentials", "true")
headers.Set("Vary", "Origin")
return true
func (h *DashboardHandler) applyCORS(c *gin.Context) bool {
headers := c.Writer.Header()
headers.Del("Access-Control-Allow-Origin")
headers.Del("Access-Control-Allow-Credentials")
Comment on lines +28 to +30
// literal "*" entry opts into permissive mode for non-browser clients; even
// then the response echoes the request Origin rather than "*", because the
// SSE EventSource API ignores wildcards when credentials are involved. See #347.
Comment on lines +177 to +192
origin := c.GetHeader("Origin")
if origin == "" {
return true
}

allowed := false
for _, o := range h.allowedOrigins {
if o == "*" || o == origin {
allowed = true
break
}
}
if !allowed {
c.AbortWithStatus(http.StatusForbidden)
return false
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dashboard SSE endpoint uses wildcard Access-Control-Allow-Origin

2 participants