Merge commit from fork

* sec: do not expose traffic stats to all users, harden origin check in websocket endpoint

* add tests to validate new logic
This commit is contained in:
h44z
2026-06-05 20:13:18 +02:00
committed by GitHub
parent e3dc31a133
commit 316f389f11
3 changed files with 271 additions and 4 deletions

View File

@@ -3,6 +3,7 @@ package handlers
import (
"context"
"net/http"
"net/url"
"strings"
"sync"
@@ -19,23 +20,28 @@ type WebsocketEventBus interface {
Unsubscribe(topic string, fn any) error
}
type WebsocketPeerService interface {
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
}
type WebsocketEndpoint struct {
authenticator Authenticator
bus WebsocketEventBus
peerService WebsocketPeerService
upgrader websocket.Upgrader
}
func NewWebsocketEndpoint(cfg *config.Config, auth Authenticator, bus WebsocketEventBus) *WebsocketEndpoint {
func NewWebsocketEndpoint(cfg *config.Config, auth Authenticator, bus WebsocketEventBus, peerService WebsocketPeerService) *WebsocketEndpoint {
return &WebsocketEndpoint{
authenticator: auth,
bus: bus,
peerService: peerService,
upgrader: websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin")
return strings.HasPrefix(origin, cfg.Web.ExternalUrl)
return matchOrigin(cfg.Web.ExternalUrl, r.Header.Get("Origin"))
},
},
}
@@ -57,6 +63,8 @@ type wsMessage struct {
func (e WebsocketEndpoint) handleWebsocket() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userInfo := domain.GetUserInfo(r.Context())
conn, err := e.upgrader.Upgrade(w, r, nil)
if err != nil {
return
@@ -74,9 +82,29 @@ func (e WebsocketEndpoint) handleWebsocket() http.HandlerFunc {
}
peerStatsHandler := func(status domain.TrafficDelta) {
if !userInfo.IsAdmin {
// lookup peer user-info to validate ownership
peer, err := e.peerService.GetPeer(ctx, domain.PeerIdentifier(status.EntityId))
if err != nil {
return
}
if peer.UserIdentifier == "" {
return // if peer is not assigned to any user, dont send stats
}
if peer.UserIdentifier != userInfo.Id {
return // only expose stats for own peers
}
}
_ = writeJSON(wsMessage{Type: "peer_stats", Data: status})
}
interfaceStatsHandler := func(status domain.TrafficDelta) {
if !userInfo.IsAdmin {
return // interface stats will only be exposed to admins
}
_ = writeJSON(wsMessage{Type: "interface_stats", Data: status})
}
@@ -98,3 +126,18 @@ func (e WebsocketEndpoint) handleWebsocket() http.HandlerFunc {
<-ctx.Done()
}
}
func matchOrigin(externalBaseUrl, origin string) bool {
originURL, err := url.Parse(origin)
if err != nil {
return false
}
externalURL, err := url.Parse(externalBaseUrl)
if err != nil {
return false
}
return originURL.Scheme == externalURL.Scheme &&
strings.EqualFold(originURL.Host, externalURL.Host)
}