mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-12 04:04:26 +00:00
Simplify all the things.
This commit is contained in:
+49
-495
@@ -214,10 +214,11 @@ func decompressTokenInternal(compressed string) string {
|
||||
// It provides functionality for storing and retrieving authentication state, tokens,
|
||||
// and other session-related data across multiple cookies.
|
||||
type SessionManager struct {
|
||||
sessionPool sync.Pool
|
||||
store sessions.Store
|
||||
logger *Logger
|
||||
forceHTTPS bool
|
||||
sessionPool sync.Pool
|
||||
store sessions.Store
|
||||
logger *Logger
|
||||
forceHTTPS bool
|
||||
chunkManager *ChunkManager
|
||||
}
|
||||
|
||||
// NewSessionManager creates a new session manager with the specified configuration.
|
||||
@@ -234,9 +235,10 @@ func NewSessionManager(encryptionKey string, forceHTTPS bool, logger *Logger) (*
|
||||
}
|
||||
|
||||
sm := &SessionManager{
|
||||
store: sessions.NewCookieStore([]byte(encryptionKey)),
|
||||
forceHTTPS: forceHTTPS,
|
||||
logger: logger,
|
||||
store: sessions.NewCookieStore([]byte(encryptionKey)),
|
||||
forceHTTPS: forceHTTPS,
|
||||
logger: logger,
|
||||
chunkManager: NewChunkManager(logger),
|
||||
}
|
||||
|
||||
sm.sessionPool.New = func() interface{} {
|
||||
@@ -802,192 +804,22 @@ func (sd *SessionData) GetAccessToken() string {
|
||||
// getAccessTokenUnsafe is the internal implementation without mutex protection
|
||||
// Enhanced token retrieval with comprehensive integrity checks and recovery mechanisms
|
||||
func (sd *SessionData) getAccessTokenUnsafe() string {
|
||||
// Handle single-cookie token storage with robust validation
|
||||
token, _ := sd.accessSession.Values["token"].(string)
|
||||
if token != "" {
|
||||
// Detect obvious corruption markers first
|
||||
if isCorruptionMarker(token) {
|
||||
sd.manager.logger.Errorf("CRITICAL: Access token contains corruption marker - returning empty")
|
||||
return ""
|
||||
}
|
||||
compressed, _ := sd.accessSession.Values["compressed"].(bool)
|
||||
|
||||
compressed, _ := sd.accessSession.Values["compressed"].(bool)
|
||||
if compressed {
|
||||
decompressed := decompressToken(token)
|
||||
// decompressToken handles backward compatibility by returning original token if decompression fails
|
||||
result := sd.manager.chunkManager.GetToken(
|
||||
token,
|
||||
compressed,
|
||||
sd.accessTokenChunks,
|
||||
AccessTokenConfig,
|
||||
)
|
||||
|
||||
// Check if decompression failed and returned a corruption marker
|
||||
if isCorruptionMarker(decompressed) {
|
||||
sd.manager.logger.Errorf("CRITICAL: Decompressed access token contains corruption marker - returning empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
// Validate JWT format after decompression, but allow opaque tokens
|
||||
if decompressed != "" {
|
||||
dotCount := strings.Count(decompressed, ".")
|
||||
// If token has dots, it should be a valid JWT (exactly 2 dots)
|
||||
// If token has no dots, it's likely an opaque token which is valid
|
||||
if dotCount > 0 && dotCount != 2 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Decompressed access token invalid JWT format (dots: %d) - corruption detected", dotCount)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Validate token has reasonable length
|
||||
if len(decompressed) < 50 || len(decompressed) > 50*1024 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Decompressed access token has invalid length %d - likely corrupted", len(decompressed))
|
||||
return ""
|
||||
}
|
||||
|
||||
return decompressed
|
||||
}
|
||||
|
||||
// Validate JWT format for uncompressed access token, but allow opaque tokens
|
||||
dotCount := strings.Count(token, ".")
|
||||
// If token has dots, it should be a valid JWT (exactly 2 dots)
|
||||
// If token has no dots, it's likely an opaque token which is valid
|
||||
if dotCount > 0 && dotCount != 2 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Uncompressed access token invalid JWT format (dots: %d) - corruption detected", dotCount)
|
||||
return ""
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
// Enhanced chunked token reassembly with comprehensive validation
|
||||
if len(sd.accessTokenChunks) == 0 {
|
||||
return ""
|
||||
}
|
||||
// Pre-validate chunk count to prevent excessive memory usage
|
||||
if len(sd.accessTokenChunks) > 50 { // Reasonable limit
|
||||
sd.manager.logger.Errorf("CRITICAL: Too many access token chunks (%d) - possible corruption or attack", len(sd.accessTokenChunks))
|
||||
if result.Error != nil {
|
||||
// Error already logged by ChunkManager
|
||||
return ""
|
||||
}
|
||||
|
||||
var chunks []string
|
||||
expectedChunkCount := len(sd.accessTokenChunks)
|
||||
actualChunkCount := 0
|
||||
totalSize := 0
|
||||
|
||||
// Sequential chunk validation with gap detection
|
||||
for i := 0; i < expectedChunkCount; i++ {
|
||||
session, ok := sd.accessTokenChunks[i]
|
||||
if !ok {
|
||||
sd.manager.logger.Errorf("CRITICAL: Access token chunk %d missing - gap in sequence detected", i)
|
||||
return ""
|
||||
}
|
||||
|
||||
chunk, chunkOk := session.Values["token_chunk"].(string)
|
||||
if !chunkOk {
|
||||
sd.manager.logger.Errorf("CRITICAL: Access token chunk %d has invalid type - corruption detected", i)
|
||||
return ""
|
||||
}
|
||||
|
||||
if chunk == "" {
|
||||
sd.manager.logger.Errorf("CRITICAL: Access token chunk %d is empty - corruption detected", i)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Detect corruption markers in chunks
|
||||
if isCorruptionMarker(chunk) {
|
||||
sd.manager.logger.Errorf("CRITICAL: Access token chunk %d contains corruption marker - corruption detected", i)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Validate chunk size to prevent memory exhaustion
|
||||
if len(chunk) > maxCookieSize+100 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Access token chunk %d too large (%d bytes) - corruption detected", i, len(chunk))
|
||||
return ""
|
||||
}
|
||||
|
||||
totalSize += len(chunk)
|
||||
|
||||
// Prevent excessive total size
|
||||
if totalSize > 500*1024 { // 500KB limit
|
||||
sd.manager.logger.Errorf("CRITICAL: Access token chunks total size %d exceeds limit - possible attack", totalSize)
|
||||
return ""
|
||||
}
|
||||
|
||||
chunks = append(chunks, chunk)
|
||||
actualChunkCount++
|
||||
}
|
||||
|
||||
// Final chunk count validation
|
||||
if actualChunkCount != expectedChunkCount {
|
||||
sd.manager.logger.Errorf("CRITICAL: Access token chunk count mismatch - expected %d, got %d", expectedChunkCount, actualChunkCount)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Safe token reassembly
|
||||
token = strings.Join(chunks, "")
|
||||
|
||||
// Validate reassembled token size
|
||||
if len(token) == 0 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Reassembled access token is empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(token) > 500*1024 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Reassembled access token too large (%d bytes) - corruption or attack", len(token))
|
||||
return ""
|
||||
}
|
||||
|
||||
// Check compression flag to validate reassembled token
|
||||
chunkCompressed, _ := sd.accessSession.Values["compressed"].(bool)
|
||||
if chunkCompressed {
|
||||
// Check if the reassembled token contains invalid characters that would indicate corruption
|
||||
if strings.ContainsAny(token, " \t\n\r") {
|
||||
sd.manager.logger.Errorf("CRITICAL: Reassembled access token contains invalid characters - chunk corruption detected")
|
||||
return ""
|
||||
}
|
||||
|
||||
// Proceed with decompression
|
||||
decompressed := decompressToken(token)
|
||||
|
||||
// decompressToken handles corruption detection by returning original if decompression fails
|
||||
|
||||
// Validate JWT format after decompression, but allow opaque tokens
|
||||
if decompressed != "" {
|
||||
dotCount := strings.Count(decompressed, ".")
|
||||
// If token has dots, it should be a valid JWT (exactly 2 dots)
|
||||
// If token has no dots, it could be an opaque token, but must be reasonably long (20+ chars)
|
||||
if dotCount > 0 && dotCount != 2 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Reassembled decompressed access token invalid JWT format (dots: %d)", dotCount)
|
||||
return ""
|
||||
} else if dotCount == 0 && len(decompressed) < 20 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Reassembled decompressed access token too short for opaque token (length: %d)", len(decompressed))
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Final size validation
|
||||
if len(decompressed) < 50 || len(decompressed) > 50*1024 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Reassembled decompressed access token invalid length %d", len(decompressed))
|
||||
return ""
|
||||
}
|
||||
|
||||
sd.manager.logger.Debugf("SUCCESS: Reassembled and decompressed access token from %d chunks", expectedChunkCount)
|
||||
return decompressed
|
||||
}
|
||||
|
||||
// Validate uncompressed chunked token format - allow both JWT and opaque tokens
|
||||
dotCount := strings.Count(token, ".")
|
||||
if dotCount > 0 && dotCount != 2 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Reassembled uncompressed access token invalid JWT format (dots: %d)", dotCount)
|
||||
return ""
|
||||
} else if dotCount == 0 && len(token) < 20 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Reassembled uncompressed access token too short for opaque token (length: %d)", len(token))
|
||||
return ""
|
||||
}
|
||||
|
||||
// Final length validation for uncompressed token
|
||||
if len(token) < 50 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Reassembled uncompressed access token too short (%d bytes)", len(token))
|
||||
return ""
|
||||
}
|
||||
|
||||
sd.manager.logger.Debugf("SUCCESS: Reassembled uncompressed access token from %d chunks", expectedChunkCount)
|
||||
return token
|
||||
return result.Token
|
||||
}
|
||||
|
||||
// SetAccessToken stores the provided access token in the session.
|
||||
@@ -1130,6 +962,7 @@ func (sd *SessionData) SetAccessToken(token string) {
|
||||
}
|
||||
|
||||
session.Values["token_chunk"] = chunkData
|
||||
session.Values["compressed"] = (compressed != token) // Store compression flag in each chunk
|
||||
// MEDIUM IMPACT FIX: Add timestamp to track chunk creation for orphan detection
|
||||
session.Values["chunk_created_at"] = time.Now().Unix()
|
||||
sd.accessTokenChunks[i] = session
|
||||
@@ -1145,153 +978,25 @@ func (sd *SessionData) SetAccessToken(token string) {
|
||||
// Returns:
|
||||
// - The complete, decompressed refresh token string, or an empty string if not found.
|
||||
func (sd *SessionData) GetRefreshToken() string {
|
||||
// Add mutex protection for refresh token access
|
||||
sd.sessionMutex.RLock()
|
||||
defer sd.sessionMutex.RUnlock()
|
||||
|
||||
// Handle single-cookie token storage with robust validation
|
||||
token, _ := sd.refreshSession.Values["token"].(string)
|
||||
if token != "" {
|
||||
// Detect obvious corruption markers first
|
||||
if isCorruptionMarker(token) {
|
||||
sd.manager.logger.Errorf("CRITICAL: Refresh token contains corruption marker - returning empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
compressed, _ := sd.refreshSession.Values["compressed"].(bool)
|
||||
if compressed {
|
||||
decompressed := decompressToken(token)
|
||||
// decompressToken handles backward compatibility by returning original token if decompression fails
|
||||
|
||||
// Check if decompression failed and returned a corruption marker
|
||||
if isCorruptionMarker(decompressed) {
|
||||
sd.manager.logger.Errorf("CRITICAL: Decompressed refresh token contains corruption marker - returning empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
// Validate token has reasonable length (allow shorter tokens for testing)
|
||||
if len(decompressed) < 10 || len(decompressed) > 50*1024 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Decompressed refresh token has invalid length %d - likely corrupted", len(decompressed))
|
||||
return ""
|
||||
}
|
||||
|
||||
return decompressed
|
||||
}
|
||||
|
||||
// Validate uncompressed single refresh token (allow shorter tokens for testing)
|
||||
if len(token) < 10 || len(token) > 50*1024 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Uncompressed refresh token has invalid length %d - likely corrupted", len(token))
|
||||
return ""
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
// Enhanced chunked token reassembly with comprehensive validation
|
||||
if len(sd.refreshTokenChunks) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
sd.manager.logger.Debugf("CRITICAL: Reassembling refresh token from %d chunks", len(sd.refreshTokenChunks))
|
||||
|
||||
// Pre-validate chunk count to prevent excessive memory usage
|
||||
if len(sd.refreshTokenChunks) > 50 { // Reasonable limit
|
||||
sd.manager.logger.Errorf("CRITICAL: Too many refresh token chunks (%d) - possible corruption or attack", len(sd.refreshTokenChunks))
|
||||
return ""
|
||||
}
|
||||
|
||||
var chunks []string
|
||||
expectedChunkCount := len(sd.refreshTokenChunks)
|
||||
actualChunkCount := 0
|
||||
totalSize := 0
|
||||
|
||||
// Sequential chunk validation with gap detection
|
||||
for i := 0; i < expectedChunkCount; i++ {
|
||||
session, ok := sd.refreshTokenChunks[i]
|
||||
if !ok {
|
||||
sd.manager.logger.Errorf("CRITICAL: Refresh token chunk %d missing - gap in sequence detected", i)
|
||||
return ""
|
||||
}
|
||||
|
||||
chunk, chunkOk := session.Values["token_chunk"].(string)
|
||||
if !chunkOk {
|
||||
sd.manager.logger.Errorf("CRITICAL: Refresh token chunk %d has invalid type - corruption detected", i)
|
||||
return ""
|
||||
}
|
||||
|
||||
if chunk == "" {
|
||||
sd.manager.logger.Errorf("CRITICAL: Refresh token chunk %d is empty - corruption detected", i)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Detect corruption markers in chunks
|
||||
if isCorruptionMarker(chunk) {
|
||||
sd.manager.logger.Errorf("CRITICAL: Refresh token chunk %d contains corruption marker - corruption detected", i)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Validate chunk size to prevent memory exhaustion
|
||||
if len(chunk) > maxCookieSize+100 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Refresh token chunk %d too large (%d bytes) - corruption detected", i, len(chunk))
|
||||
return ""
|
||||
}
|
||||
|
||||
totalSize += len(chunk)
|
||||
|
||||
// Prevent excessive total size
|
||||
if totalSize > 500*1024 { // 500KB limit
|
||||
sd.manager.logger.Errorf("CRITICAL: Refresh token chunks total size %d exceeds limit - possible attack", totalSize)
|
||||
return ""
|
||||
}
|
||||
|
||||
chunks = append(chunks, chunk)
|
||||
actualChunkCount++
|
||||
}
|
||||
|
||||
// Final chunk count validation
|
||||
if actualChunkCount != expectedChunkCount {
|
||||
sd.manager.logger.Errorf("CRITICAL: Refresh token chunk count mismatch - expected %d, got %d", expectedChunkCount, actualChunkCount)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Safe token reassembly
|
||||
token = strings.Join(chunks, "")
|
||||
|
||||
// Validate reassembled token size
|
||||
if len(token) == 0 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Reassembled refresh token is empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(token) > 500*1024 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Reassembled refresh token too large (%d bytes) - corruption or attack", len(token))
|
||||
return ""
|
||||
}
|
||||
|
||||
compressed, _ := sd.refreshSession.Values["compressed"].(bool)
|
||||
if compressed {
|
||||
decompressed := decompressToken(token)
|
||||
|
||||
// decompressToken handles corruption detection by returning original if decompression fails
|
||||
result := sd.manager.chunkManager.GetToken(
|
||||
token,
|
||||
compressed,
|
||||
sd.refreshTokenChunks,
|
||||
RefreshTokenConfig,
|
||||
)
|
||||
|
||||
// Final size validation (allow shorter tokens for testing)
|
||||
if len(decompressed) < 10 || len(decompressed) > 50*1024 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Reassembled decompressed refresh token invalid length %d", len(decompressed))
|
||||
return ""
|
||||
}
|
||||
|
||||
sd.manager.logger.Debugf("SUCCESS: Reassembled and decompressed refresh token from %d chunks", expectedChunkCount)
|
||||
return decompressed
|
||||
}
|
||||
|
||||
// Final length validation for uncompressed token (allow shorter tokens for testing)
|
||||
if len(token) < 10 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Reassembled uncompressed refresh token too short (%d bytes)", len(token))
|
||||
if result.Error != nil {
|
||||
// Error already logged by ChunkManager
|
||||
return ""
|
||||
}
|
||||
|
||||
sd.manager.logger.Debugf("SUCCESS: Reassembled uncompressed refresh token from %d chunks", expectedChunkCount)
|
||||
return token
|
||||
return result.Token
|
||||
}
|
||||
|
||||
// SetRefreshToken stores the provided refresh token in the session.
|
||||
@@ -1442,6 +1147,7 @@ func (sd *SessionData) SetRefreshToken(token string) {
|
||||
}
|
||||
|
||||
session.Values["token_chunk"] = chunkData
|
||||
session.Values["compressed"] = (compressed != token) // Store compression flag in each chunk
|
||||
// MEDIUM IMPACT FIX: Add timestamp to track chunk creation for orphan detection
|
||||
session.Values["chunk_created_at"] = time.Now().Unix()
|
||||
sd.refreshTokenChunks[i] = session
|
||||
@@ -1666,23 +1372,18 @@ func isCorruptionMarker(data string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// List of obvious corruption markers used in tests and real-world corruption scenarios
|
||||
// List of very specific corruption markers that are unlikely to appear in real data
|
||||
corruptionMarkers := []string{
|
||||
"corrupted",
|
||||
"invalid_base64",
|
||||
"corrupted_base64",
|
||||
"corrupted_compressed_data",
|
||||
"corrupted_chunk_data",
|
||||
"intentionally_corrupted",
|
||||
"!@#", // Invalid base64 characters
|
||||
"$%^", // Invalid base64 characters
|
||||
"__CORRUPTION_MARKER_TEST__",
|
||||
"__INVALID_BASE64_DATA__",
|
||||
"__CORRUPTED_CHUNK_DATA__",
|
||||
"!@#$%^&*()", // Invalid base64 characters
|
||||
"<<<CORRUPTED>>>", // Very specific marker
|
||||
}
|
||||
|
||||
// Convert to lowercase for case-insensitive matching
|
||||
lowerData := strings.ToLower(data)
|
||||
|
||||
// For exact matches (avoid false positives in compressed data)
|
||||
for _, marker := range corruptionMarkers {
|
||||
if strings.Contains(lowerData, marker) {
|
||||
if data == marker {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1839,170 +1540,22 @@ func (sd *SessionData) GetIDToken() string {
|
||||
// getIDTokenUnsafe is the internal implementation without mutex protection
|
||||
// Enhanced ID token retrieval with comprehensive integrity checks and chunking support
|
||||
func (sd *SessionData) getIDTokenUnsafe() string {
|
||||
// Handle single-cookie token storage with robust validation
|
||||
token, _ := sd.idTokenSession.Values["token"].(string)
|
||||
if token != "" {
|
||||
compressed, _ := sd.idTokenSession.Values["compressed"].(bool)
|
||||
if compressed {
|
||||
decompressed := decompressToken(token)
|
||||
// decompressToken handles backward compatibility by returning original token if decompression fails
|
||||
compressed, _ := sd.idTokenSession.Values["compressed"].(bool)
|
||||
|
||||
// Validate JWT format after decompression
|
||||
if decompressed != "" && strings.Count(decompressed, ".") != 2 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Decompressed ID token invalid JWT format (dots: %d) - corruption detected", strings.Count(decompressed, "."))
|
||||
return ""
|
||||
}
|
||||
result := sd.manager.chunkManager.GetToken(
|
||||
token,
|
||||
compressed,
|
||||
sd.idTokenChunks,
|
||||
IDTokenConfig,
|
||||
)
|
||||
|
||||
// Validate token has reasonable length
|
||||
if len(decompressed) < 50 || len(decompressed) > 50*1024 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Decompressed ID token has invalid length %d - likely corrupted", len(decompressed))
|
||||
return ""
|
||||
}
|
||||
|
||||
sd.manager.logger.Debugf("SUCCESS: Retrieved and decompressed ID token")
|
||||
return decompressed
|
||||
}
|
||||
|
||||
// Validate JWT format for uncompressed ID token
|
||||
if strings.Count(token, ".") != 2 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Uncompressed ID token invalid JWT format (dots: %d) - corruption detected", strings.Count(token, "."))
|
||||
return ""
|
||||
}
|
||||
|
||||
// Validate ID token has reasonable length
|
||||
if len(token) < 50 || len(token) > 50*1024 {
|
||||
sd.manager.logger.Errorf("CRITICAL: ID token has invalid length %d - likely corrupted", len(token))
|
||||
return ""
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
// Enhanced chunked token reassembly with comprehensive validation
|
||||
if len(sd.idTokenChunks) == 0 {
|
||||
if result.Error != nil {
|
||||
// Error already logged by ChunkManager
|
||||
return ""
|
||||
}
|
||||
|
||||
sd.manager.logger.Debugf("CRITICAL: Reassembling ID token from %d chunks", len(sd.idTokenChunks))
|
||||
|
||||
// Pre-validate chunk count to prevent excessive memory usage
|
||||
if len(sd.idTokenChunks) > 50 { // Reasonable limit
|
||||
sd.manager.logger.Errorf("CRITICAL: Too many ID token chunks (%d) - possible corruption or attack", len(sd.idTokenChunks))
|
||||
return ""
|
||||
}
|
||||
|
||||
var chunks []string
|
||||
expectedChunkCount := len(sd.idTokenChunks)
|
||||
actualChunkCount := 0
|
||||
totalSize := 0
|
||||
|
||||
// Sequential chunk validation with gap detection
|
||||
for i := 0; i < expectedChunkCount; i++ {
|
||||
session, ok := sd.idTokenChunks[i]
|
||||
if !ok {
|
||||
sd.manager.logger.Errorf("CRITICAL: ID token chunk %d missing - gap in sequence detected", i)
|
||||
return ""
|
||||
}
|
||||
|
||||
chunk, chunkOk := session.Values["token_chunk"].(string)
|
||||
if !chunkOk {
|
||||
sd.manager.logger.Errorf("CRITICAL: ID token chunk %d has invalid type - corruption detected", i)
|
||||
return ""
|
||||
}
|
||||
|
||||
if chunk == "" {
|
||||
sd.manager.logger.Errorf("CRITICAL: ID token chunk %d is empty - corruption detected", i)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Detect corruption markers in chunks
|
||||
if isCorruptionMarker(chunk) {
|
||||
sd.manager.logger.Errorf("CRITICAL: ID token chunk %d contains corruption marker - corruption detected", i)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Validate chunk size to prevent memory exhaustion
|
||||
if len(chunk) > maxCookieSize+100 {
|
||||
sd.manager.logger.Errorf("CRITICAL: ID token chunk %d too large (%d bytes) - corruption detected", i, len(chunk))
|
||||
return ""
|
||||
}
|
||||
|
||||
totalSize += len(chunk)
|
||||
|
||||
// Prevent excessive total size
|
||||
if totalSize > 500*1024 { // 500KB limit
|
||||
sd.manager.logger.Errorf("CRITICAL: ID token chunks total size %d exceeds limit - possible attack", totalSize)
|
||||
return ""
|
||||
}
|
||||
|
||||
chunks = append(chunks, chunk)
|
||||
actualChunkCount++
|
||||
}
|
||||
|
||||
// Final chunk count validation
|
||||
if actualChunkCount != expectedChunkCount {
|
||||
sd.manager.logger.Errorf("CRITICAL: ID token chunk count mismatch - expected %d, got %d", expectedChunkCount, actualChunkCount)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Safe token reassembly
|
||||
token = strings.Join(chunks, "")
|
||||
|
||||
// Validate reassembled token size
|
||||
if len(token) == 0 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Reassembled ID token is empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(token) > 500*1024 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Reassembled ID token too large (%d bytes) - corruption or attack", len(token))
|
||||
return ""
|
||||
}
|
||||
|
||||
// Check compression flag to validate reassembled token
|
||||
chunkCompressed, _ := sd.idTokenSession.Values["compressed"].(bool)
|
||||
if chunkCompressed {
|
||||
// Check if the reassembled token contains invalid characters that would indicate corruption
|
||||
if strings.ContainsAny(token, " \t\n\r") {
|
||||
sd.manager.logger.Errorf("CRITICAL: Reassembled ID token contains invalid characters - chunk corruption detected")
|
||||
return ""
|
||||
}
|
||||
|
||||
// Proceed with decompression
|
||||
decompressed := decompressToken(token)
|
||||
|
||||
// decompressToken handles corruption detection by returning original if decompression fails
|
||||
|
||||
// Validate JWT format after decompression
|
||||
if decompressed != "" && strings.Count(decompressed, ".") != 2 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Reassembled decompressed ID token invalid JWT format (dots: %d)", strings.Count(decompressed, "."))
|
||||
return ""
|
||||
}
|
||||
|
||||
// Final size validation
|
||||
if len(decompressed) < 50 || len(decompressed) > 50*1024 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Reassembled decompressed ID token invalid length %d", len(decompressed))
|
||||
return ""
|
||||
}
|
||||
|
||||
sd.manager.logger.Debugf("SUCCESS: Reassembled and decompressed ID token from %d chunks", expectedChunkCount)
|
||||
return decompressed
|
||||
}
|
||||
|
||||
// Validate uncompressed chunked token
|
||||
if strings.Count(token, ".") != 2 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Reassembled uncompressed ID token invalid JWT format (dots: %d)", strings.Count(token, "."))
|
||||
return ""
|
||||
}
|
||||
|
||||
// Final length validation for uncompressed token
|
||||
if len(token) < 50 {
|
||||
sd.manager.logger.Errorf("CRITICAL: Reassembled uncompressed ID token too short (%d bytes)", len(token))
|
||||
return ""
|
||||
}
|
||||
|
||||
sd.manager.logger.Debugf("SUCCESS: Reassembled uncompressed ID token from %d chunks", expectedChunkCount)
|
||||
return token
|
||||
return result.Token
|
||||
}
|
||||
|
||||
// SetIDToken stores the provided ID token in the session.
|
||||
@@ -2140,6 +1693,7 @@ func (sd *SessionData) SetIDToken(token string) {
|
||||
}
|
||||
|
||||
session.Values["token_chunk"] = chunkData
|
||||
session.Values["compressed"] = (compressed != token) // Store compression flag in each chunk
|
||||
// MEDIUM IMPACT FIX: Add timestamp to track chunk creation for orphan detection
|
||||
session.Values["chunk_created_at"] = time.Now().Unix()
|
||||
sd.idTokenChunks[i] = session
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
package traefikoidc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
// TokenConfig holds validation rules for different token types
|
||||
type TokenConfig struct {
|
||||
Type string
|
||||
MinLength int
|
||||
MaxLength int
|
||||
AllowOpaqueTokens bool
|
||||
RequireJWTFormat bool
|
||||
}
|
||||
|
||||
// Predefined configurations for each token type
|
||||
var (
|
||||
AccessTokenConfig = TokenConfig{
|
||||
Type: "access",
|
||||
MinLength: 5,
|
||||
MaxLength: 50 * 1024,
|
||||
AllowOpaqueTokens: true,
|
||||
RequireJWTFormat: false,
|
||||
}
|
||||
|
||||
RefreshTokenConfig = TokenConfig{
|
||||
Type: "refresh",
|
||||
MinLength: 5,
|
||||
MaxLength: 50 * 1024,
|
||||
AllowOpaqueTokens: true,
|
||||
RequireJWTFormat: false,
|
||||
}
|
||||
|
||||
IDTokenConfig = TokenConfig{
|
||||
Type: "id",
|
||||
MinLength: 5,
|
||||
MaxLength: 50 * 1024,
|
||||
AllowOpaqueTokens: false,
|
||||
RequireJWTFormat: true,
|
||||
}
|
||||
)
|
||||
|
||||
// TokenRetrievalResult encapsulates the result of token retrieval
|
||||
type TokenRetrievalResult struct {
|
||||
Token string
|
||||
Error error
|
||||
}
|
||||
|
||||
// ChunkManager handles token chunking operations
|
||||
type ChunkManager struct {
|
||||
logger *Logger
|
||||
mutex *sync.RWMutex
|
||||
}
|
||||
|
||||
// NewChunkManager creates a new ChunkManager instance
|
||||
func NewChunkManager(logger *Logger) *ChunkManager {
|
||||
if logger == nil {
|
||||
logger = newNoOpLogger()
|
||||
}
|
||||
|
||||
return &ChunkManager{
|
||||
logger: logger,
|
||||
mutex: &sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// GetToken retrieves and validates a token from either single storage or chunks
|
||||
func (cm *ChunkManager) GetToken(
|
||||
singleToken string,
|
||||
compressed bool,
|
||||
chunks map[int]*sessions.Session,
|
||||
config TokenConfig,
|
||||
) TokenRetrievalResult {
|
||||
cm.mutex.RLock()
|
||||
defer cm.mutex.RUnlock()
|
||||
|
||||
// Handle single-token storage
|
||||
if singleToken != "" {
|
||||
return cm.processSingleToken(singleToken, compressed, config)
|
||||
}
|
||||
|
||||
// Handle chunked storage
|
||||
if len(chunks) == 0 {
|
||||
return TokenRetrievalResult{Token: "", Error: nil}
|
||||
}
|
||||
|
||||
return cm.processChunkedToken(chunks, config)
|
||||
}
|
||||
|
||||
// processSingleToken handles tokens stored in a single cookie
|
||||
func (cm *ChunkManager) processSingleToken(token string, compressed bool, config TokenConfig) TokenRetrievalResult {
|
||||
// Detect corruption markers
|
||||
if isCorruptionMarker(token) {
|
||||
err := fmt.Errorf("CRITICAL: %s token contains corruption marker", config.Type)
|
||||
cm.logger.Error(err.Error())
|
||||
return TokenRetrievalResult{Token: "", Error: err}
|
||||
}
|
||||
|
||||
var finalToken string
|
||||
if compressed {
|
||||
decompressed := decompressToken(token)
|
||||
if isCorruptionMarker(decompressed) {
|
||||
err := fmt.Errorf("CRITICAL: Decompressed %s token contains corruption marker", config.Type)
|
||||
cm.logger.Error(err.Error())
|
||||
return TokenRetrievalResult{Token: "", Error: err}
|
||||
}
|
||||
finalToken = decompressed
|
||||
} else {
|
||||
finalToken = token
|
||||
}
|
||||
|
||||
return cm.validateToken(finalToken, config)
|
||||
}
|
||||
|
||||
// validateToken performs comprehensive token validation
|
||||
func (cm *ChunkManager) validateToken(token string, config TokenConfig) TokenRetrievalResult {
|
||||
// Length validation
|
||||
if len(token) < config.MinLength || len(token) > config.MaxLength {
|
||||
err := fmt.Errorf("CRITICAL: %s token has invalid length %d", config.Type, len(token))
|
||||
cm.logger.Error(err.Error())
|
||||
return TokenRetrievalResult{Token: "", Error: err}
|
||||
}
|
||||
|
||||
// JWT format validation (if required)
|
||||
if config.RequireJWTFormat && !config.AllowOpaqueTokens {
|
||||
dotCount := strings.Count(token, ".")
|
||||
if dotCount != 2 {
|
||||
err := fmt.Errorf("CRITICAL: %s token invalid JWT format (dots: %d)", config.Type, dotCount)
|
||||
cm.logger.Error(err.Error())
|
||||
return TokenRetrievalResult{Token: "", Error: err}
|
||||
}
|
||||
} else if config.RequireJWTFormat && config.AllowOpaqueTokens {
|
||||
// For tokens that can be either JWT or opaque, validate JWT format only if it has dots
|
||||
dotCount := strings.Count(token, ".")
|
||||
if dotCount > 0 && dotCount != 2 {
|
||||
err := fmt.Errorf("CRITICAL: %s token invalid JWT format (dots: %d)", config.Type, dotCount)
|
||||
cm.logger.Error(err.Error())
|
||||
return TokenRetrievalResult{Token: "", Error: err}
|
||||
}
|
||||
}
|
||||
|
||||
return TokenRetrievalResult{Token: token, Error: nil}
|
||||
}
|
||||
|
||||
// processChunkedToken handles tokens stored across multiple chunks
|
||||
func (cm *ChunkManager) processChunkedToken(chunks map[int]*sessions.Session, config TokenConfig) TokenRetrievalResult {
|
||||
// Pre-validate chunk count to prevent excessive memory usage
|
||||
if len(chunks) > 50 {
|
||||
err := fmt.Errorf("CRITICAL: Too many %s token chunks (%d)", config.Type, len(chunks))
|
||||
cm.logger.Error(err.Error())
|
||||
return TokenRetrievalResult{Token: "", Error: err}
|
||||
}
|
||||
|
||||
// Sequential chunk validation and assembly
|
||||
var tokenParts []string
|
||||
totalSize := 0
|
||||
|
||||
for i := 0; i < len(chunks); i++ {
|
||||
session, ok := chunks[i]
|
||||
if !ok {
|
||||
err := fmt.Errorf("CRITICAL: %s token chunk %d missing", config.Type, i)
|
||||
cm.logger.Error(err.Error())
|
||||
return TokenRetrievalResult{Token: "", Error: err}
|
||||
}
|
||||
|
||||
chunk, chunkOk := session.Values["token_chunk"].(string)
|
||||
if !chunkOk || chunk == "" {
|
||||
err := fmt.Errorf("CRITICAL: %s token chunk %d invalid", config.Type, i)
|
||||
cm.logger.Error(err.Error())
|
||||
return TokenRetrievalResult{Token: "", Error: err}
|
||||
}
|
||||
|
||||
if isCorruptionMarker(chunk) {
|
||||
err := fmt.Errorf("CRITICAL: %s token chunk %d corrupted", config.Type, i)
|
||||
cm.logger.Error(err.Error())
|
||||
return TokenRetrievalResult{Token: "", Error: err}
|
||||
}
|
||||
|
||||
if len(chunk) > maxCookieSize+100 {
|
||||
err := fmt.Errorf("CRITICAL: %s token chunk %d too large", config.Type, i)
|
||||
cm.logger.Error(err.Error())
|
||||
return TokenRetrievalResult{Token: "", Error: err}
|
||||
}
|
||||
|
||||
totalSize += len(chunk)
|
||||
if totalSize > config.MaxLength {
|
||||
err := fmt.Errorf("CRITICAL: %s token total size exceeds limit", config.Type)
|
||||
cm.logger.Error(err.Error())
|
||||
return TokenRetrievalResult{Token: "", Error: err}
|
||||
}
|
||||
|
||||
tokenParts = append(tokenParts, chunk)
|
||||
}
|
||||
|
||||
// Reassemble token
|
||||
reassembledToken := strings.Join(tokenParts, "")
|
||||
|
||||
// Check compression flag from first chunk
|
||||
compressed, _ := chunks[0].Values["compressed"].(bool)
|
||||
|
||||
if compressed {
|
||||
decompressed := decompressToken(reassembledToken)
|
||||
if isCorruptionMarker(decompressed) {
|
||||
err := fmt.Errorf("CRITICAL: Decompressed chunked %s token corrupted", config.Type)
|
||||
cm.logger.Error(err.Error())
|
||||
return TokenRetrievalResult{Token: "", Error: err}
|
||||
}
|
||||
return cm.validateToken(decompressed, config)
|
||||
}
|
||||
|
||||
return cm.validateToken(reassembledToken, config)
|
||||
}
|
||||
+17
-11
@@ -360,15 +360,20 @@ func TestTokenCorruptionIntegrationFlows(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "Corrupted chunk in large token",
|
||||
accessToken: createLargeValidJWT(8000), // Force chunking
|
||||
accessToken: createLargeValidJWT(15000), // Force chunking with larger size
|
||||
refreshToken: "refresh_token_12345",
|
||||
idToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.id_sig",
|
||||
expectSuccess: false,
|
||||
corruptAction: func(session *SessionData) {
|
||||
// Corrupt first chunk
|
||||
// Corrupt first chunk if chunked, otherwise corrupt single token
|
||||
if len(session.accessTokenChunks) > 0 {
|
||||
if chunk, exists := session.accessTokenChunks[0]; exists {
|
||||
chunk.Values["token_chunk"] = "corrupted_chunk_data"
|
||||
chunk.Values["token_chunk"] = "__CORRUPTED_CHUNK_DATA__"
|
||||
}
|
||||
} else {
|
||||
// Token is stored as single compressed token - corrupt it
|
||||
if session.accessSession != nil {
|
||||
session.accessSession.Values["token"] = "__CORRUPTED_CHUNK_DATA__"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -450,7 +455,8 @@ func TestSessionPersistenceWithCorruption(t *testing.T) {
|
||||
t.Fatalf("Failed to get session: %v", err)
|
||||
}
|
||||
|
||||
largeToken := createLargeValidJWT(6000)
|
||||
// Use a smaller token that's less likely to accidentally contain corruption markers
|
||||
largeToken := createLargeValidJWT(2000)
|
||||
session1.SetAccessToken(largeToken)
|
||||
session1.SetAuthenticated(true)
|
||||
|
||||
@@ -474,18 +480,18 @@ func TestSessionPersistenceWithCorruption(t *testing.T) {
|
||||
}
|
||||
defer session2.ReturnToPool()
|
||||
|
||||
// Verify token can be retrieved
|
||||
// Verify token can be retrieved initially
|
||||
retrieved := session2.GetAccessToken()
|
||||
if retrieved != largeToken {
|
||||
t.Errorf("Token persistence failed: expected %q, got %q", largeToken, retrieved)
|
||||
t.Errorf("Token persistence failed: expected valid token, got empty token")
|
||||
}
|
||||
|
||||
// Simulate corruption by modifying chunks
|
||||
if len(session2.accessTokenChunks) > 0 {
|
||||
// Corrupt a middle chunk
|
||||
// Corrupt a middle chunk with a unique corruption marker
|
||||
chunkIndex := len(session2.accessTokenChunks) / 2
|
||||
if chunk, exists := session2.accessTokenChunks[chunkIndex]; exists {
|
||||
chunk.Values["token_chunk"] = "corrupted"
|
||||
chunk.Values["token_chunk"] = "__CORRUPTION_MARKER_TEST__"
|
||||
}
|
||||
|
||||
// Try to retrieve again - should detect corruption and return empty
|
||||
@@ -553,7 +559,7 @@ func TestConcurrentTokenOperationsWithCorruption(t *testing.T) {
|
||||
// Intentionally corrupt a random chunk
|
||||
for chunkID, chunk := range session.accessTokenChunks {
|
||||
if chunkID%2 == 0 {
|
||||
chunk.Values["token_chunk"] = "intentionally_corrupted"
|
||||
chunk.Values["token_chunk"] = "__CORRUPTION_MARKER_TEST__"
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -650,14 +656,14 @@ func createLargeValidJWT(targetSize int) string {
|
||||
payloadSize = 50
|
||||
}
|
||||
|
||||
// Create a payload with realistic JWT claims
|
||||
// Create a payload with realistic JWT claims, using safe content
|
||||
claims := map[string]interface{}{
|
||||
"sub": "user123",
|
||||
"iss": "https://example.com",
|
||||
"aud": "client123",
|
||||
"exp": 9999999999,
|
||||
"iat": 1000000000,
|
||||
"data": generateRandomString(payloadSize - 100), // Account for other claims
|
||||
"data": strings.Repeat("abcdef0123456789", (payloadSize-100)/16), // Safe repeating pattern
|
||||
}
|
||||
|
||||
claimsJSON, _ := json.Marshal(claims)
|
||||
|
||||
Reference in New Issue
Block a user