Skip to content

Commit 5865aa7

Browse files
authored
fix: restrict session deletion to negative MaxAge values (#93)
* fix: restrict session deletion to negative MaxAge values - Only allow session deletion if MaxAge is negative, not zero Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com> * test: improve session cookie handling and Redis integration - Add regression test to verify session cookies (MaxAge=0) are created without Max-Age attribute and saved to Redis with the default TTL - Ensure session retrieval from Redis preserves values for session cookies - Confirm that setting MaxAge to -1 deletes the session from Redis Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com> --------- Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
1 parent e471101 commit 5865aa7

2 files changed

Lines changed: 128 additions & 1 deletion

File tree

redistore.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -795,7 +795,7 @@ func (s *RediStore) New(r *http.Request, name string) (*sessions.Session, error)
795795
// Save adds a single session to the response.
796796
func (s *RediStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
797797
// Marked for deletion.
798-
if session.Options.MaxAge <= 0 {
798+
if session.Options.MaxAge < 0 {
799799
if err := s.delete(session); err != nil {
800800
return err
801801
}

redistore_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,133 @@ func TestNewStore_WithURL(t *testing.T) {
441441
})
442442
}
443443

444+
// TestSessionCookieMaxAgeZero tests that MaxAge == 0 creates a session cookie
445+
// (no Max-Age attribute) and saves to Redis with DefaultMaxAge TTL.
446+
// This is a regression test for issue #53.
447+
func TestSessionCookieMaxAgeZero(t *testing.T) {
448+
addr := setup()
449+
store := createTestStore(t, addr)
450+
defer func() {
451+
if err := store.Close(); err != nil {
452+
fmt.Printf("Error closing store: %v\n", err)
453+
}
454+
}()
455+
456+
var cookies []string
457+
458+
// Round 1: Create a session with MaxAge = 0 (session cookie)
459+
t.Run("Create session cookie with MaxAge=0", func(t *testing.T) {
460+
req, _ := http.NewRequestWithContext(
461+
context.Background(), "GET", "http://localhost:8080/", nil)
462+
rsp := NewRecorder()
463+
session := getSession(t, store, req)
464+
465+
// Set MaxAge to 0 to create a session cookie
466+
session.Options.MaxAge = 0
467+
session.Values["user"] = "testuser"
468+
session.Values["authenticated"] = true
469+
470+
saveSession(t, req, rsp)
471+
cookies = getCookies(t, rsp)
472+
473+
// Verify cookie is set
474+
if len(cookies) == 0 {
475+
t.Fatal("Expected cookie to be set")
476+
}
477+
478+
// Verify the cookie doesn't contain an explicit Max-Age=0
479+
// (session cookies should not have Max-Age attribute set to 0)
480+
cookieStr := cookies[0]
481+
if bytes.Contains([]byte(cookieStr), []byte("Max-Age=0")) {
482+
t.Errorf("Session cookie should not have Max-Age=0, got: %s", cookieStr)
483+
}
484+
485+
// Verify session was saved to Redis by checking if we can retrieve it
486+
conn := store.Pool.Get()
487+
defer conn.Close()
488+
489+
// Get the session ID from the session
490+
if session.ID == "" {
491+
t.Fatal("Session ID should not be empty after save")
492+
}
493+
494+
// Check if the key exists in Redis
495+
exists, err := conn.Do("EXISTS", store.keyPrefix+session.ID)
496+
if err != nil {
497+
t.Fatalf("Error checking Redis key existence: %v", err)
498+
}
499+
if exists == int64(0) {
500+
t.Error("Session should be saved to Redis when MaxAge=0")
501+
}
502+
503+
// Verify the TTL is set to DefaultMaxAge (not 0)
504+
ttl, err := conn.Do("TTL", store.keyPrefix+session.ID)
505+
if err != nil {
506+
t.Fatalf("Error getting TTL from Redis: %v", err)
507+
}
508+
ttlInt := ttl.(int64)
509+
if ttlInt <= 0 {
510+
t.Errorf("Expected positive TTL (DefaultMaxAge), got %d", ttlInt)
511+
}
512+
// TTL should be close to DefaultMaxAge (1200 seconds / 20 minutes)
513+
// Allow some margin for test execution time
514+
if ttlInt < 1190 || ttlInt > 1200 {
515+
t.Errorf("Expected TTL close to DefaultMaxAge (1200), got %d", ttlInt)
516+
}
517+
})
518+
519+
// Round 2: Verify the session can be retrieved
520+
t.Run("Retrieve session cookie", func(t *testing.T) {
521+
req, _ := http.NewRequestWithContext(
522+
context.Background(), "GET", "http://localhost:8080/", nil)
523+
req.Header.Add("Cookie", cookies[0])
524+
session := getSession(t, store, req)
525+
526+
// Verify session is not new (it was loaded from Redis)
527+
if session.IsNew {
528+
t.Error("Session should not be new, it should be loaded from Redis")
529+
}
530+
531+
// Verify session values are preserved
532+
user, ok := session.Values["user"]
533+
if !ok || user != "testuser" {
534+
t.Errorf("Expected user='testuser', got %v", user)
535+
}
536+
537+
authenticated, ok := session.Values["authenticated"]
538+
if !ok || authenticated != true {
539+
t.Errorf("Expected authenticated=true, got %v", authenticated)
540+
}
541+
})
542+
543+
// Round 3: Verify MaxAge < 0 deletes the session (existing behavior)
544+
t.Run("Delete session with MaxAge=-1", func(t *testing.T) {
545+
req, _ := http.NewRequestWithContext(
546+
context.Background(), "GET", "http://localhost:8080/", nil)
547+
req.Header.Add("Cookie", cookies[0])
548+
rsp := NewRecorder()
549+
session := getSession(t, store, req)
550+
551+
sessionID := session.ID
552+
553+
// Set MaxAge to -1 to delete the session
554+
session.Options.MaxAge = -1
555+
saveSession(t, req, rsp)
556+
557+
// Verify session was deleted from Redis
558+
conn := store.Pool.Get()
559+
defer conn.Close()
560+
561+
exists, err := conn.Do("EXISTS", store.keyPrefix+sessionID)
562+
if err != nil {
563+
t.Fatalf("Error checking Redis key existence: %v", err)
564+
}
565+
if exists != int64(0) {
566+
t.Error("Session should be deleted from Redis when MaxAge=-1")
567+
}
568+
})
569+
}
570+
444571
func ExampleRediStore() {
445572
// RedisStore
446573
store, err := NewStore(

0 commit comments

Comments
 (0)