Skip to content

fix: Implement WebSocket origin checking in production#3273

Open
salignatmoandal wants to merge 3 commits intosuperplanehq:mainfrom
salignatmoandal:fix/websocket-origin-checking
Open

fix: Implement WebSocket origin checking in production#3273
salignatmoandal wants to merge 3 commits intosuperplanehq:mainfrom
salignatmoandal:fix/websocket-origin-checking

Conversation

@salignatmoandal
Copy link
Copy Markdown

What

In production, WebSocket connections are now restricted to allowed origins only. In development, all origins are still accepted (no change for local dev).

Why

Previously, the server accepted WebSocket connections from any origin. That could allow a third-party site to open a WebSocket to your SuperPlane instance using the user's cookies. Restricting origins in production reduces that risk and aligns with common security practice for WebSockets.

How

  • allowedWebSocketOrigins() builds the list of allowed origins:
    • In development (APP_ENV=development): returns no list → all origins accepted (unchanged behavior).
    • In production: uses WEBSOCKET_ALLOWED_ORIGINS if set (comma-separated), otherwise derives a single origin from BASE_URL (scheme + host).
  • The WebSocket CheckOrigin callback now allows a connection only when:
    • there is no restriction list (dev / fallback), or
    • the request’s Origin header is in the allowed list.

Configuration

  • BASE_URL: In production, the origin derived from this URL is allowed (e.g. https://app.example.comhttps://app.example.com).
  • WEBSOCKET_ALLOWED_ORIGINS (optional): Comma-separated list of origins when you need multiple (e.g. https://app.example.com,https://dashboard.example.com).
  • No change needed for local development; all origins remain accepted when APP_ENV=development.

Signed-off-by: Mawen Salignat-Moandal <mwnslgt@gmail.com>
@salignatmoandal salignatmoandal force-pushed the fix/websocket-origin-checking branch from 90cfb6b to cf89f62 Compare February 25, 2026 22:38
Comment thread pkg/public/server.go Outdated
origin := strings.TrimSpace(r.Header.Get("Origin"))
if origin == "" {
return false
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Missing Origin header blocks non-browser WebSocket clients

Medium Severity

When allowedOrigins is configured (production), connections without an Origin header are rejected. The gorilla/websocket default behavior is to accept connections when the Origin header is absent, because non-browser clients (CLI tools, server-to-server) typically don't send it. The Origin header is a browser-only CSRF mechanism, so rejecting its absence blocks legitimate programmatic clients without any security benefit — non-browser clients can trivially forge the header anyway.

Fix in Cursor Fix in Web

@shiroyasha
Copy link
Copy Markdown
Collaborator

Hey @salignatmoandal 🙌 I like to idea of this PR.

I would like to see some small code cleanup before we merge it.

1/ Lets create a new file dedicated to this
2/ Instead of adding low level details in the main server code, lets extract the logic into the other file as much as possible
3/ In what scenarios we want anything other than BASE_URL? 🤔 Do we have a real use for that additional environment variable?

A closure could work well for this use case. e.g.:

// pkg/server/websocket_origin_checker.go

func WebsocketOriginChecker() func(*http.Request) bool {
  var allowedOrigins
  // calculate allowed origins here ...

  return func(r *http.Request) bool {
    // verify here    
  }
}

// pkg/server/server.go

server := &Server{
  upgrader: &websocket.Upgrader{
    CheckOrigin: WebsocketOriginChecker()
  }
}

@shiroyasha shiroyasha changed the title fix:Implement WebSocket origin checking in production fix: Implement WebSocket origin checking in production Mar 13, 2026
@AleksandarCole AleksandarCole requested review from forestileao and shiroyasha and removed request for forestileao March 16, 2026 10:48
@AleksandarCole AleksandarCole added the pr:stage-3/3 Ready for full, in-depth, review label Mar 16, 2026
Extract websocket origin validation to a dedicated helper and wire it via CheckOrigin closure.
Use BASE_URL origin in production while keeping development unrestricted.
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment thread pkg/public/websocket_origin.go Outdated
return false
}

return origin == allowedOrigin
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Case-sensitive origin comparison breaks with non-lowercase BASE_URL

Low Severity

The origin comparison origin == allowedOrigin is case-sensitive, but RFC 6454 requires ASCII case-insensitive origin comparison, and gorilla/websocket's own default checkSameOrigin uses equalASCIIFold. Since Go's url.Parse does not normalize scheme or host to lowercase, a BASE_URL like HTTPS://App.Example.com would produce an allowedOrigin with uppercase characters, while browsers always send lowercase Origin headers — causing all WebSocket connections to be rejected in production.

Fix in Cursor Fix in Web

@salignatmoandal
Copy link
Copy Markdown
Author

Yep, I followed that approach.

I moved the WebSocket origin logic to a dedicated file, and server.go now only wires CheckOrigin via a closure (newWebSocketCheckOrigin(appEnv, baseURL)), so the low-level details are out of the main server setup.

I also simplified the config to rely on BASE_URL in production, while keeping development unrestricted.

@cursor
Copy link
Copy Markdown
Contributor

cursor Bot commented Mar 19, 2026

You have used all of your free Bugbot PR reviews.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:stage-3/3 Ready for full, in-depth, review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants