fix: set Secure/HttpOnly/SameSite on session cookies (closes #47) #48

Merged
sneak merged 3 commits from fix/gosec-findings into main 2026-08-07 17:41:03 +02:00
5 changed files with 68 additions and 85 deletions
Showing only changes of commit cb9e14eee7 - Show all commits

View File

@@ -56,9 +56,6 @@ returns 410; logout redirects back to login
- P0: implement cache size management and eviction so the disk cannot - P0: implement cache size management and eviction so the disk cannot
fill up fill up
- P0: validate configuration on startup, fail fast on bad config - P0: validate configuration on startup, fail fast on bad config
- P1: remove the ignored former secure toggle parameter from
session.NewManager and update its call sites (requires touching
existing tests; needs approval per repo rules)
- P1: implement blocked networks configuration to extend SSRF - P1: implement blocked networks configuration to extend SSRF
protection protection
- P1: rate limit global concurrent upstream fetches to prevent - P1: rate limit global concurrent upstream fetches to prevent

View File

@@ -95,9 +95,8 @@ func (s *Handlers) initImageService() error {
s.log.Info("image service initialized") s.log.Info("image service initialized")
// Initialize session manager (signing key is validated at config load // Initialize session manager (signing key is validated at config load
// time). The second argument is ignored: session cookies are always // time). Session cookies are always Secure/HttpOnly/SameSite=Strict.
// Secure/HttpOnly/SameSite=Strict. sessMgr, err := session.NewManager(s.config.SigningKey)
sessMgr, err := session.NewManager(s.config.SigningKey, true)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -44,10 +44,8 @@ type Manager struct {
// Session cookies always carry the Secure, HttpOnly, and SameSite=Strict // Session cookies always carry the Secure, HttpOnly, and SameSite=Strict
// attributes; this cannot be configured. Browsers treat http://localhost as a // attributes; this cannot be configured. Browsers treat http://localhost as a
// trustworthy origin and accept Secure cookies there, so local development // trustworthy origin and accept Secure cookies there, so local development
// keeps working. The second parameter is the former secure toggle: it is // keeps working.
// ignored and retained only so existing call sites keep compiling; it will be func NewManager(signingKey string) (*Manager, error) {
// removed in a follow-up change.
func NewManager(signingKey string, _ bool) (*Manager, error) {
masterKey := []byte(signingKey) masterKey := []byte(signingKey)
// Derive separate keys for HMAC (hash) and encryption (block) // Derive separate keys for HMAC (hash) and encryption (block)

View File

@@ -8,17 +8,15 @@ import (
// TestSessionCookieAttributesAlwaysSecure verifies that every cookie // TestSessionCookieAttributesAlwaysSecure verifies that every cookie
// emitted by the session manager carries HttpOnly, Secure, and a // emitted by the session manager carries HttpOnly, Secure, and a
// SameSite mode of Lax or stricter, regardless of how the manager was // SameSite mode of Lax or stricter. Session cookies contain the
// constructed. Session cookies contain the authentication state and // authentication state and must never be exposed to script (HttpOnly),
// must never be exposed to script (HttpOnly), sent over plaintext HTTP // sent over plaintext HTTP (Secure), or attached to cross-site
// (Secure), or attached to cross-site requests (SameSite). No // requests (SameSite). Nothing may weaken these attributes.
// constructor argument may weaken these attributes.
// //
// This covers both cookie-writing paths: CreateSession (the login // This covers both cookie-writing paths: CreateSession (the login
// set-cookie path) and ClearSession (the logout delete-cookie path). // set-cookie path) and ClearSession (the logout delete-cookie path).
func TestSessionCookieAttributesAlwaysSecure(t *testing.T) { func TestSessionCookieAttributesAlwaysSecure(t *testing.T) {
for _, constructorBoolArg := range []bool{false, true} { mgr, err := NewManager("test-signing-key-12345")
mgr, err := NewManager("test-signing-key-12345", constructorBoolArg)
if err != nil { if err != nil {
t.Fatalf("NewManager() error = %v", err) t.Fatalf("NewManager() error = %v", err)
} }
@@ -46,14 +44,7 @@ func TestSessionCookieAttributesAlwaysSecure(t *testing.T) {
} }
for _, writePath := range writePaths { for _, writePath := range writePaths {
testName := writePath.name t.Run(writePath.name, func(t *testing.T) {
if constructorBoolArg {
testName += "/constructorBoolArg=true"
} else {
testName += "/constructorBoolArg=false"
}
t.Run(testName, func(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
writePath.setCookie(t, w) writePath.setCookie(t, w)
@@ -89,4 +80,3 @@ func TestSessionCookieAttributesAlwaysSecure(t *testing.T) {
}) })
} }
} }
}

View File

@@ -8,7 +8,7 @@ import (
) )
func TestManager_CreateAndValidate(t *testing.T) { func TestManager_CreateAndValidate(t *testing.T) {
mgr, err := NewManager("test-signing-key-12345", false) mgr, err := NewManager("test-signing-key-12345")
if err != nil { if err != nil {
t.Fatalf("NewManager() error = %v", err) t.Fatalf("NewManager() error = %v", err)
} }
@@ -57,7 +57,7 @@ func TestManager_CreateAndValidate(t *testing.T) {
} }
func TestManager_ValidateSession_NoCookie(t *testing.T) { func TestManager_ValidateSession_NoCookie(t *testing.T) {
mgr, _ := NewManager("test-signing-key-12345", false) mgr, _ := NewManager("test-signing-key-12345")
req := httptest.NewRequest(http.MethodGet, "/", nil) req := httptest.NewRequest(http.MethodGet, "/", nil)
@@ -72,7 +72,7 @@ func TestManager_ValidateSession_NoCookie(t *testing.T) {
} }
func TestManager_ValidateSession_TamperedCookie(t *testing.T) { func TestManager_ValidateSession_TamperedCookie(t *testing.T) {
mgr, _ := NewManager("test-signing-key-12345", false) mgr, _ := NewManager("test-signing-key-12345")
req := httptest.NewRequest(http.MethodGet, "/", nil) req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{ req.AddCookie(&http.Cookie{
@@ -91,8 +91,8 @@ func TestManager_ValidateSession_TamperedCookie(t *testing.T) {
} }
func TestManager_ValidateSession_WrongKey(t *testing.T) { func TestManager_ValidateSession_WrongKey(t *testing.T) {
mgr1, _ := NewManager("signing-key-1", false) mgr1, _ := NewManager("signing-key-1")
mgr2, _ := NewManager("signing-key-2", false) mgr2, _ := NewManager("signing-key-2")
// Create session with mgr1 // Create session with mgr1
w := httptest.NewRecorder() w := httptest.NewRecorder()
@@ -118,7 +118,7 @@ func TestManager_ValidateSession_WrongKey(t *testing.T) {
} }
func TestManager_ClearSession(t *testing.T) { func TestManager_ClearSession(t *testing.T) {
mgr, _ := NewManager("test-signing-key-12345", false) mgr, _ := NewManager("test-signing-key-12345")
w := httptest.NewRecorder() w := httptest.NewRecorder()
mgr.ClearSession(w) mgr.ClearSession(w)
@@ -144,7 +144,7 @@ func TestManager_ClearSession(t *testing.T) {
} }
func TestManager_IsAuthenticated(t *testing.T) { func TestManager_IsAuthenticated(t *testing.T) {
mgr, _ := NewManager("test-signing-key-12345", false) mgr, _ := NewManager("test-signing-key-12345")
// No session - should return false // No session - should return false
req := httptest.NewRequest(http.MethodGet, "/", nil) req := httptest.NewRequest(http.MethodGet, "/", nil)
@@ -175,8 +175,7 @@ func TestManager_IsAuthenticated(t *testing.T) {
} }
func TestManager_CookieAttributes(t *testing.T) { func TestManager_CookieAttributes(t *testing.T) {
// Test with secure=true mgr, _ := NewManager("test-key")
mgr, _ := NewManager("test-key", true)
w := httptest.NewRecorder() w := httptest.NewRecorder()
_ = mgr.CreateSession(w) _ = mgr.CreateSession(w)