diff --git a/review_r61_chunk_total_test.go b/review_r61_chunk_total_test.go new file mode 100644 index 0000000..e4748d0 --- /dev/null +++ b/review_r61_chunk_total_test.go @@ -0,0 +1,49 @@ +package traefikoidc + +import ( + "strings" + "testing" + + "github.com/gorilla/sessions" +) + +// TestProcessChunkedTokenDetectsTrailingTruncation guards against silently +// trusting a truncated session token. The recombiner previously used +// len(chunks) as the authoritative total, so losing the trailing chunk(s) +// (browser cookie eviction, dropped Set-Cookie) reassembled the prefix with +// no error. Now the write path stores token_total and the reader rejects a +// count mismatch. +func TestProcessChunkedTokenDetectsTrailingTruncation(t *testing.T) { + cm := NewChunkManager(NewLogger(DefaultLogLevel)) + config := TokenConfig{ + Type: "access", + MinLength: 5, + MaxLength: 100 * 1024, + MaxChunks: 25, + MaxChunkSize: maxCookieSize, + } + + // 3 chunks were written (token_total=3), but only 2 remain (trailing lost). + chunks := map[int]*sessions.Session{ + 0: {Values: map[interface{}]interface{}{"token_chunk": "aaaa", "token_total": 3}}, + 1: {Values: map[interface{}]interface{}{"token_chunk": "bbbb", "token_total": 3}}, + } + + res := cm.GetToken("", false, chunks, config) + if res.Error == nil { + t.Fatalf("expected truncation error when chunk count < stored total") + } + if !strings.Contains(res.Error.Error(), "chunk count mismatch") { + t.Fatalf("expected chunk-count-mismatch error, got: %v", res.Error) + } + + // Control: complete set still assembles (no truncation error). + full := map[int]*sessions.Session{ + 0: {Values: map[interface{}]interface{}{"token_chunk": "aaaa", "token_total": 2}}, + 1: {Values: map[interface{}]interface{}{"token_chunk": "bbbb", "token_total": 2}}, + } + res2 := cm.GetToken("", false, full, config) + if res2.Error != nil && strings.Contains(res2.Error.Error(), "chunk count mismatch") { + t.Fatalf("complete chunk set should not report truncation: %v", res2.Error) + } +} diff --git a/session.go b/session.go index e8541dc..b23fcfd 100644 --- a/session.go +++ b/session.go @@ -2026,6 +2026,7 @@ func (sd *SessionData) SetAccessToken(token string) { session.Values["token_chunk"] = chunkData session.Values["compressed"] = (compressed != token) session.Values["chunk_created_at"] = time.Now().Unix() + session.Values["token_total"] = len(chunks) sd.accessTokenChunks[i] = session } @@ -2211,6 +2212,7 @@ func (sd *SessionData) SetRefreshToken(token string) { session.Values["token_chunk"] = chunkData session.Values["compressed"] = (compressed != token) session.Values["chunk_created_at"] = time.Now().Unix() + session.Values["token_total"] = len(chunks) sd.refreshTokenChunks[i] = session } @@ -2836,6 +2838,7 @@ func (sd *SessionData) SetIDToken(token string) { session.Values["token_chunk"] = chunkData session.Values["compressed"] = (compressed != token) session.Values["chunk_created_at"] = time.Now().Unix() + session.Values["token_total"] = len(chunks) sd.idTokenChunks[i] = session } diff --git a/session_chunk_manager.go b/session_chunk_manager.go index 04637e5..6c98534 100644 --- a/session_chunk_manager.go +++ b/session_chunk_manager.go @@ -360,6 +360,20 @@ func (cm *ChunkManager) processChunkedToken(chunks map[int]*sessions.Session, co return TokenRetrievalResult{Token: "", Error: err} } + // Verify the chunk count matches the total recorded when the token was + // written. Without this, silently losing the trailing chunk(s) (browser + // cookie eviction, dropped Set-Cookie, attacker dropping a cookie) would + // reassemble a truncated prefix with no error and trust it as the token. + // The write path stores token_total on every chunk; check it on chunk 0. + if total, ok := chunks[0].Values["token_total"].(int); ok { + if len(chunks) != total { + err := fmt.Errorf("%s token chunk count mismatch: got %d, expected %d (possibly truncated)", + config.Type, len(chunks), total) + cm.logger.Errorf("Truncated %s token chunks: got %d, expected %d", config.Type, len(chunks), total) + return TokenRetrievalResult{Token: "", Error: err} + } + } + // Sequential chunk validation and assembly var tokenParts []string totalSize := 0