mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
API tokens passed via ?token= query parameter were accepted on all HTTP requests. This is a security concern because tokens in URLs can leak via server logs, browser history, referrer headers, and proxy logs. The query-string token path exists solely for WebSocket connections which cannot set custom headers during the upgrade handshake. This change adds an isWebSocketUpgrade check to all three query-string extraction sites in CheckAuth and extractAndStoreAuthContext, rejecting ?token= on regular HTTP requests while preserving WebSocket functionality. No frontend impact — the kiosk flow stores the token in sessionStorage then uses X-API-Token headers for all API calls.
113 lines
3.4 KiB
Go
113 lines
3.4 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
)
|
|
|
|
func TestIsWebSocketUpgrade(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
upgrade string
|
|
expected bool
|
|
}{
|
|
{"lowercase websocket", "websocket", true},
|
|
{"uppercase WebSocket", "WebSocket", true},
|
|
{"mixed case WEBSOCKET", "WEBSOCKET", true},
|
|
{"empty header", "", false},
|
|
{"other value", "h2c", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
if tt.upgrade != "" {
|
|
req.Header.Set("Upgrade", tt.upgrade)
|
|
}
|
|
if got := isWebSocketUpgrade(req); got != tt.expected {
|
|
t.Errorf("isWebSocketUpgrade() = %v, want %v", got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckAuth_QueryTokenRejectedWithoutWebSocketUpgrade(t *testing.T) {
|
|
// API-only mode: only API tokens configured, no user/password.
|
|
const rawToken = "test-api-only-query-token-12345"
|
|
|
|
record, err := config.NewAPITokenRecord(rawToken, "test", nil)
|
|
if err != nil {
|
|
t.Fatalf("create token record: %v", err)
|
|
}
|
|
|
|
cfg := &config.Config{
|
|
APITokens: []config.APITokenRecord{*record},
|
|
}
|
|
cfg.SortAPITokens()
|
|
|
|
// Regular HTTP request with query-string token should be rejected.
|
|
req := httptest.NewRequest(http.MethodGet, "/api/state?token="+rawToken, nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
if CheckAuth(cfg, rr, req) {
|
|
t.Fatal("expected CheckAuth to reject query-string token on regular HTTP request (API-only mode)")
|
|
}
|
|
|
|
// Same request with WebSocket Upgrade header should be accepted.
|
|
wsReq := httptest.NewRequest(http.MethodGet, "/api/state?token="+rawToken, nil)
|
|
wsReq.Header.Set("Upgrade", "websocket")
|
|
wsReq.Header.Set("Connection", "Upgrade")
|
|
wsRR := httptest.NewRecorder()
|
|
|
|
if !CheckAuth(cfg, wsRR, wsReq) {
|
|
t.Fatal("expected CheckAuth to accept query-string token on WebSocket upgrade (API-only mode)")
|
|
}
|
|
}
|
|
|
|
func TestCheckAuth_QueryTokenRejectedStandardMode(t *testing.T) {
|
|
// Standard mode: both password auth and API tokens configured.
|
|
const rawToken = "test-standard-query-token-12345"
|
|
|
|
record, err := config.NewAPITokenRecord(rawToken, "test", nil)
|
|
if err != nil {
|
|
t.Fatalf("create token record: %v", err)
|
|
}
|
|
|
|
cfg := &config.Config{
|
|
AuthUser: "admin",
|
|
AuthPass: "$2a$10$invalidhashfortesting1234567890123456789012",
|
|
APITokens: []config.APITokenRecord{*record},
|
|
}
|
|
cfg.SortAPITokens()
|
|
|
|
// Regular HTTP request with query-string token should be rejected.
|
|
req := httptest.NewRequest(http.MethodGet, "/api/state?token="+rawToken, nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
if CheckAuth(cfg, rr, req) {
|
|
t.Fatal("expected CheckAuth to reject query-string token on regular HTTP request (standard mode)")
|
|
}
|
|
|
|
// WebSocket upgrade with query-string token should be accepted.
|
|
wsReq := httptest.NewRequest(http.MethodGet, "/api/state?token="+rawToken, nil)
|
|
wsReq.Header.Set("Upgrade", "websocket")
|
|
wsReq.Header.Set("Connection", "Upgrade")
|
|
wsRR := httptest.NewRecorder()
|
|
|
|
if !CheckAuth(cfg, wsRR, wsReq) {
|
|
t.Fatal("expected CheckAuth to accept query-string token on WebSocket upgrade (standard mode)")
|
|
}
|
|
|
|
// Header-based token should still work for regular requests.
|
|
headerReq := httptest.NewRequest(http.MethodGet, "/api/state", nil)
|
|
headerReq.Header.Set("X-API-Token", rawToken)
|
|
headerRR := httptest.NewRecorder()
|
|
|
|
if !CheckAuth(cfg, headerRR, headerReq) {
|
|
t.Fatal("expected CheckAuth to accept header token on regular HTTP request")
|
|
}
|
|
}
|