mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
R68: cache expiry TOCTOU, logout open-redirect, session max-age
This commit is contained in:
Vendored
+4
-2
@@ -179,8 +179,10 @@ func (m *MemoryCacheBackend) Get(ctx context.Context, key string) (interface{},
|
||||
value, exists, expired := shard.get(key)
|
||||
|
||||
if expired {
|
||||
// Clean up expired item
|
||||
shard.delete(key)
|
||||
// Clean up expired item. Use deleteIfExpired so a concurrent Set
|
||||
// that refreshed this key between get() and here is not deleted
|
||||
// (which would drop a freshly-written value = lost update).
|
||||
shard.deleteIfExpired(key)
|
||||
m.misses.Add(1)
|
||||
return nil, ErrCacheMiss
|
||||
}
|
||||
|
||||
+18
@@ -117,6 +117,24 @@ func (s *cacheShard) delete(key string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// deleteIfExpired removes a key only if the entry still present is expired.
|
||||
// It is safe to call after observing an expired item via get(): a
|
||||
// concurrent set() that refreshed the key between the read and this call
|
||||
// leaves the fresh value intact. Unlike delete, it avoids deleting a
|
||||
// just-written value (a lost update in Get's expiry cleanup).
|
||||
func (s *cacheShard) deleteIfExpired(key string) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
item, exists := s.items[key]
|
||||
if !exists || !item.isExpired() {
|
||||
return false
|
||||
}
|
||||
|
||||
s.deleteItemLocked(item)
|
||||
return true
|
||||
}
|
||||
|
||||
// exists checks if a key exists (and is not expired)
|
||||
func (s *cacheShard) exists(key string) bool {
|
||||
s.mu.RLock()
|
||||
|
||||
+55
@@ -781,3 +781,58 @@ func TestMatchPattern(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMemoryShard_DeleteIfExpiredPreservesConcurrentRefresh regresses a
|
||||
// TOCTOU in MemoryCacheBackend.Get's expired-item cleanup. Get reads an
|
||||
// item, sees it expired (shard.get), then was deleting it with an
|
||||
// unconditional shard.delete in a separate critical section. If a
|
||||
// concurrent Set refreshed the key between the read and the delete, the
|
||||
// delete removed the freshly-written value (a lost update — the key
|
||||
// vanished right after it was refreshed). Get now uses deleteIfExpired,
|
||||
// which deletes only if the entry is still expired, leaving a concurrent
|
||||
// refresh intact. This test models that interleaving deterministically.
|
||||
func TestMemoryShard_DeleteIfExpiredPreservesConcurrentRefresh(t *testing.T) {
|
||||
s := newCacheShard(0, 0)
|
||||
const key = "k"
|
||||
|
||||
// Get's first phase: observe an already-expired entry.
|
||||
s.set(key, "old", time.Now().Add(-time.Second), 1)
|
||||
if _, _, expired := s.get(key); !expired {
|
||||
t.Fatal("precondition: seeded item must be expired")
|
||||
}
|
||||
|
||||
// A concurrent refresh lands a fresh value inside the get->delete window.
|
||||
s.set(key, "fresh", time.Now().Add(time.Hour), 1)
|
||||
|
||||
// Get's cleanup must not remove the fresh value.
|
||||
if s.deleteIfExpired(key) {
|
||||
t.Fatal("deleteIfExpired removed a value a concurrent Set just wrote")
|
||||
}
|
||||
if v, _, _ := s.get(key); v != "fresh" {
|
||||
t.Fatalf("fresh value was lost by expiry cleanup, got %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMemoryShard_DeleteClobbersConcurrentRefresh documents the failure
|
||||
// mode the fix addresses: the old unconditional delete removes the value
|
||||
// a concurrent Set just wrote under the same interleaving. If
|
||||
// deleteIfExpired ever regresses to unconditional deletion, the sibling
|
||||
// test above fails.
|
||||
func TestMemoryShard_DeleteClobbersConcurrentRefresh(t *testing.T) {
|
||||
s := newCacheShard(0, 0)
|
||||
const key = "k"
|
||||
|
||||
s.set(key, "old", time.Now().Add(-time.Second), 1)
|
||||
if _, _, expired := s.get(key); !expired {
|
||||
t.Fatal("precondition: seeded item must be expired")
|
||||
}
|
||||
s.set(key, "fresh", time.Now().Add(time.Hour), 1)
|
||||
|
||||
// Old cleanup (delete) removes the just-written value: lost update.
|
||||
if !s.delete(key) {
|
||||
t.Log("old delete found no item")
|
||||
}
|
||||
if v, _, _ := s.get(key); v == "fresh" {
|
||||
t.Log("note: value happened to survive, but delete is unconditional")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user