Summary
The CacheService.lock() implementation in api/src/cache/cache.service.ts:86-106 stores a static value 1 and does unconditional DEL in the finally block. If the lock expires mid-operation, another caller may have acquired the lock, and the first caller's finally block deletes the second caller's lock.
Findings
Impact
Under high concurrency or slow callbacks, mutual exclusion guarantees are violated. This affects all code paths using cache.lock(), including server assignment and matchmaking verification.
Suggested Fix
- Generate a UUID per lock acquisition and store it as the lock value
- Use a Lua script for conditional delete:
if redis.call('get', KEYS[1]) == ARGV[1] then redis.call('del', KEYS[1]) end
- The pattern is already demonstrated in
MatchmakeService.CLAIM_LOBBY_SCRIPT
References
Found during security review of Sprint 3 audit PRs (#373, #374).
Summary
The
CacheService.lock()implementation inapi/src/cache/cache.service.ts:86-106stores a static value1and does unconditionalDELin thefinallyblock. If the lock expires mid-operation, another caller may have acquired the lock, and the first caller'sfinallyblock deletes the second caller's lock.Findings
cache.service.ts:94—SET lockKey 1 EX expires NXstores a fixed value, making ownership verification impossiblecache.service.ts:98—this.forget(lockKey)does unconditionalDEL, potentially releasing another caller's lockImpact
Under high concurrency or slow callbacks, mutual exclusion guarantees are violated. This affects all code paths using
cache.lock(), including server assignment and matchmaking verification.Suggested Fix
if redis.call('get', KEYS[1]) == ARGV[1] then redis.call('del', KEYS[1]) endMatchmakeService.CLAIM_LOBBY_SCRIPTReferences
Found during security review of Sprint 3 audit PRs (#373, #374).