Skip to content

Commit fcda981

Browse files
cfsmp3claude
andcommitted
fix: secure session cookies and validate WebSocket origin
Security fixes: - Add Secure, HttpOnly, SameSite flags to session cookies - HttpOnly: prevents JavaScript access (XSS protection) - Secure: cookies sent only over HTTPS in production - SameSite=Lax: CSRF protection while allowing OAuth redirects - Validate WebSocket origin against allowed origins - Check Origin header against ALLOWED_ORIGIN env var - Allow localhost in development mode - Fallback to same-origin check if no env var configured - Log rejected connection attempts - Update example.backend.env with security settings - Add ENV=production for secure mode - Add ALLOWED_ORIGIN for WebSocket validation - Better documentation of all variables Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d64701f commit fcda981

3 files changed

Lines changed: 67 additions & 9 deletions

File tree

backend/controllers/websocket.go

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package controllers
22

33
import (
44
"net/http"
5+
"os"
6+
"strings"
57

68
"ccsync_backend/utils"
79

@@ -13,10 +15,43 @@ type JobStatus struct {
1315
Status string `json:"status"`
1416
}
1517

16-
var upgrader = websocket.Upgrader{
17-
CheckOrigin: func(r *http.Request) bool {
18+
// checkWebSocketOrigin validates the Origin header against allowed origins
19+
func checkWebSocketOrigin(r *http.Request) bool {
20+
origin := r.Header.Get("Origin")
21+
if origin == "" {
22+
// No origin header - could be same-origin or non-browser client
23+
return true
24+
}
25+
26+
// Get allowed origin from environment (e.g., "https://taskwarrior-server.ccextractor.org")
27+
allowedOrigin := os.Getenv("ALLOWED_ORIGIN")
28+
29+
// In development, allow localhost origins
30+
if os.Getenv("ENV") != "production" {
31+
if strings.HasPrefix(origin, "http://localhost") ||
32+
strings.HasPrefix(origin, "http://127.0.0.1") {
33+
return true
34+
}
35+
}
36+
37+
// Check against configured allowed origin
38+
if allowedOrigin != "" && origin == allowedOrigin {
1839
return true
19-
},
40+
}
41+
42+
// If no ALLOWED_ORIGIN configured, check if origin matches the request host
43+
// This provides same-origin protection as fallback
44+
host := r.Host
45+
if strings.Contains(origin, host) {
46+
return true
47+
}
48+
49+
utils.Logger.Warnf("WebSocket connection rejected from origin: %s", origin)
50+
return false
51+
}
52+
53+
var upgrader = websocket.Upgrader{
54+
CheckOrigin: checkWebSocketOrigin,
2055
}
2156

2257
var clients = make(map[*websocket.Conn]bool)

backend/main.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,16 @@ func main() {
8080
utils.Logger.Fatal("SESSION_KEY environment variable is not set or empty")
8181
}
8282
store := sessions.NewCookieStore(sessionKey)
83+
84+
// Configure secure cookie options
85+
store.Options = &sessions.Options{
86+
Path: "/",
87+
MaxAge: 86400 * 7, // 7 days
88+
HttpOnly: true, // Prevent JavaScript access
89+
Secure: os.Getenv("ENV") == "production", // HTTPS only in production
90+
SameSite: http.SameSiteLaxMode, // CSRF protection (Lax allows OAuth redirects)
91+
}
92+
8393
gob.Register(map[string]interface{}{})
8494

8595
app := controllers.App{Config: conf, SessionStore: store}

production/example.backend.env

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
1-
REDIRECT_URL_DEV="http://localhost:8000/auth/callback"
2-
SESSION_KEY="Random key"
3-
CLIENT_SEC="Via Google Oauth"
4-
CLIENT_ID="Via Google Oauth"
5-
FRONTEND_ORIGIN_DEV="http://localhost"
6-
CONTAINER_ORIGIN="http://production-syncserver-1:8080/"
1+
# Environment: set to "production" for secure cookies and strict origin checking
2+
ENV="production"
3+
4+
# OAuth configuration
5+
REDIRECT_URL_DEV="https://your-domain.com/auth/callback"
6+
CLIENT_ID="your-google-oauth-client-id"
7+
CLIENT_SEC="your-google-oauth-client-secret"
8+
9+
# Session configuration (generate a random 32+ character key)
10+
SESSION_KEY="generate-a-random-secret-key-here"
11+
12+
# CORS and WebSocket origin (your frontend URL, no trailing slash)
13+
FRONTEND_ORIGIN_DEV="https://your-domain.com"
14+
ALLOWED_ORIGIN="https://your-domain.com"
15+
16+
# Sync server container URL (internal Docker network)
17+
CONTAINER_ORIGIN="http://syncserver:8080/"
18+
19+
# Port (usually 8000)
720
PORT=8000

0 commit comments

Comments
 (0)