Cache Clear Endpoint Accepts Arbitrary Prefix Without Validation
DELETE /admin/v1/cache/:cachePrefix forwarded an attacker-controlled URL path parameter into a Redis SCAN MATCH → UNLINK loop, allowing a compromised admin key to wipe security-critical prefixes (liquidationExecution, settlementExecution, evmEventStreamDedup) and trigger duplicate liquidations and on-chain event reprocessing.
Description
The DELETE /admin/v1/cache/:cachePrefix endpoint takes a user-supplied URL parameter and performs a wildcard deletion across the entire Redis cache with zero validation or allowlisting.
At clear-cache.controller.ts#L6-L17, the controller is guarded only by AdminApiKeyGuard (role check) and passes the raw URL parameter directly to the service:
@UseGuards(AdminApiKeyGuard)@UsePipes(new ValidationPipe({ transform: true }))@Controller()export class ClearCacheController {constructor(private readonly clearCacheService: ClearCacheService) {}@UseGuards(AdminApiKeyGuard)@HttpCode(HttpStatus.ACCEPTED)@Delete(buildAdminApiPath(adminApiEndpoint.cache, apiPathParam.cachePrefix))public async deleteCacheEntries(@Param("cachePrefix") cachePrefix: string): Promise<void> {await this.clearCacheService.deleteAllKeysStartsWith(cachePrefix);// L16, attacker-controlled prefix, no allowlist}}
At clear-cache.service.ts#L17-L24, the service appends a colon and delegates to the cache manager:
public async deleteAllKeysStartsWith(prefix: string): Promise<void> {if (!prefix) { return; }this.logger.log(`Deleting all cache keys starting with ${prefix}`);const count = await this.cacheManagerService.deleteStartsWith(`${prefix}:`);// L23, passes "${prefix}:*" to Redis SCANthis.logger.log(`Found and deleted ${count} keys starting with ${prefix}`);}
At cache-manager.service.ts#L61-L79, the Redis SCAN iterator matches all keys with the given prefix and unlinks them one by one:
public async deleteStartsWith(prefix: string): Promise<number> {return this.getClient().execute(async (client) => {const stream = client.scanIterator({MATCH: `${prefix}*`, // L65, wildcard glob match on user-controlled prefixCOUNT: 100,});let count = 0;for await (const keysInBatch of stream) {for (const key of keysInBatch) {await client.unlink(key); // L73, deletes every matching keycount++;}}return count;});}
The system defines 55 cache entry prefixes at base-cache.type.ts#L1-L55, all of which are vulnerable to targeted deletion. The security-critical ones include cancelPositionAntiReplay, evmEventStreamDedup, evmNonce, liquidatablePositionsByMint, liquidationExecution, settlementExecution, and unwindExecution.
Vulnerable scenario:
- An attacker obtains or compromises an admin API key (or a compromised insider with admin access acts maliciously).
- The attacker calls
DELETE /admin/v1/cache/liquidationExecution. This deletes theLiquidationAntiReplayStoreentries, which useexecuteOnlyOnce()to prevent re-executing liquidations. With the anti-replay keys cleared, the next price check cycle will re-trigger liquidation flows for positions that have already been liquidated, potentially creating double-sell orders on Polymarket's CLOB and causing financial loss.
Impact
Financial Loss. Clearing liquidationExecution or settlementExecution anti-replay stores can trigger duplicate liquidation/settlement transactions, causing double-sell orders on Polymarket that result in direct financial loss from the protocol's capital pool.
State Corruption. Clearing evmEventStreamDedup causes reprocessing of on-chain events, potentially corrupting position state by applying the same state transition twice.
Recommendation
Even with the admin guard, consider implementing a strict allowlist of cache prefixes that can be cleared (e.g., only non-critical caches like offerExecutionData, inferenceMarketData, leverageThreshold).
Add audit logging with alerting for all cache clear operations.
Consider requiring a confirmation token or two-step approval for destructive cache operations.
Resolution
Fixed by code removal in commit f0379e821 (2026-04-18), the controller, service, and route constants were all deleted.

