fix(security): stream tickets instead of session tokens in URLs (FLAW-22)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m32s

WebSocket and EventSource put the 30-day session token into the URL,
where proxies log it. The app now gets a one-time ticket right before
connecting (POST /auth/stream-ticket, 60 s, bound to the token version);
?token= is no longer accepted for streams, non-browser clients may use
the Authorization header.

With BASE_URL set, CORS allows only that origin and WebSocket upgrades
only it or the requested host; credentials are off (no cookies exist).
Logout, ending all sessions, a new password and deactivation close all
live streams of the account; clients with a valid session reconnect
with a fresh ticket.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-29 14:46:58 +01:00
1 parent 5c711c2cf7
commit f57a9884d1
17 files changed
+335 -41

No files matched your search

+26 -1
View File
@@ -17,10 +17,22 @@ var upgrader = websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 4096,
CheckOrigin: func(r *http.Request) bool {
return true // Self-hosted reverse proxy compatible
return originCheck(r)
},
}
// originCheck decides which browser origins may open a WebSocket (see SetOriginCheck). Without a
// configured check every origin is accepted: proxies often rewrite the Host header, so a plain
// same-host comparison would break working setups.
var originCheck = func(*http.Request) bool { return true }
// SetOriginCheck installs the origin rule for WebSocket upgrades.
func (h *Hub) SetOriginCheck(f func(*http.Request) bool) {
if f != nil {
originCheck = f
}
}
type Client struct {
hub *Hub
conn *websocket.Conn
@@ -371,6 +383,19 @@ func (h *Hub) BroadcastToUser(userID, event string, payload interface{}) {
}
}
// DisconnectUser ends every live connection of a user (logout, all sessions ended, new password,
// account deactivated). Clients reconnect with a fresh ticket if their session is still valid.
func (h *Hub) DisconnectUser(userID string) {
h.mu.RLock()
defer h.mu.RUnlock()
for client := range h.userRooms[userID] {
client.closeSend()
}
for sseClient := range h.sseUserRooms[userID] {
sseClient.closeSend()
}
}
// BroadcastSystem sends an event to every connection. Only for events without user data (e.g. the
// demo reset notice).
func (h *Hub) BroadcastSystem(event string) {