mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-17 04:37:48 +00:00
R61: detect truncated chunked token via stored total
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2026,6 +2026,7 @@ func (sd *SessionData) SetAccessToken(token string) {
|
|||||||
session.Values["token_chunk"] = chunkData
|
session.Values["token_chunk"] = chunkData
|
||||||
session.Values["compressed"] = (compressed != token)
|
session.Values["compressed"] = (compressed != token)
|
||||||
session.Values["chunk_created_at"] = time.Now().Unix()
|
session.Values["chunk_created_at"] = time.Now().Unix()
|
||||||
|
session.Values["token_total"] = len(chunks)
|
||||||
sd.accessTokenChunks[i] = session
|
sd.accessTokenChunks[i] = session
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2211,6 +2212,7 @@ func (sd *SessionData) SetRefreshToken(token string) {
|
|||||||
session.Values["token_chunk"] = chunkData
|
session.Values["token_chunk"] = chunkData
|
||||||
session.Values["compressed"] = (compressed != token)
|
session.Values["compressed"] = (compressed != token)
|
||||||
session.Values["chunk_created_at"] = time.Now().Unix()
|
session.Values["chunk_created_at"] = time.Now().Unix()
|
||||||
|
session.Values["token_total"] = len(chunks)
|
||||||
sd.refreshTokenChunks[i] = session
|
sd.refreshTokenChunks[i] = session
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2836,6 +2838,7 @@ func (sd *SessionData) SetIDToken(token string) {
|
|||||||
session.Values["token_chunk"] = chunkData
|
session.Values["token_chunk"] = chunkData
|
||||||
session.Values["compressed"] = (compressed != token)
|
session.Values["compressed"] = (compressed != token)
|
||||||
session.Values["chunk_created_at"] = time.Now().Unix()
|
session.Values["chunk_created_at"] = time.Now().Unix()
|
||||||
|
session.Values["token_total"] = len(chunks)
|
||||||
sd.idTokenChunks[i] = session
|
sd.idTokenChunks[i] = session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -360,6 +360,20 @@ func (cm *ChunkManager) processChunkedToken(chunks map[int]*sessions.Session, co
|
|||||||
return TokenRetrievalResult{Token: "", Error: err}
|
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
|
// Sequential chunk validation and assembly
|
||||||
var tokenParts []string
|
var tokenParts []string
|
||||||
totalSize := 0
|
totalSize := 0
|
||||||
|
|||||||
Reference in New Issue
Block a user