HIGH race conditionapi keys

Race Condition with Api Keys

How Race Condition Manifests in API Keys

Race conditions in API key management occur when multiple concurrent operations manipulate the same key state without proper synchronization. This creates windows where attackers can exploit timing gaps to escalate privileges, bypass rate limits, or perform unauthorized actions.

The most common API key race condition pattern involves key validation and state mutation occurring as separate operations. Consider a key rotation system where an old key is being revoked while a new key is being activated. If these operations aren't atomic, an attacker who captures the old key during the transition window can authenticate successfully even though the system intends both keys to be invalid.

// Vulnerable pattern - non-atomic key rotation
function rotateApiKey(userId, newKeyHash) {
    const oldKey = db.getUserApiKey(userId);
    // Race window: old key still valid
    db.revokeApiKey(oldKey);
    // New key not yet active
    db.activateApiKey(newKeyHash);
}

Another critical race condition occurs in rate limiting implementations. When multiple requests hit the rate limiter simultaneously for the same API key, the counter increment operations can interleave:

// Vulnerable rate limiting - race condition possible
function checkRateLimit(apiKey) {
    const current = db.getRateLimit(apiKey);
    if (current.count >= MAX_REQUESTS) {
        return false;
    }
    // Race window: multiple threads can pass check
    db.incrementRateLimit(apiKey);
    return true;
}

During high traffic, hundreds of requests might pass the initial check before any increments occur, allowing all of them to execute despite exceeding the limit. This effectively nullifies rate limiting protections.

API key revocation also suffers from race conditions. When a user requests key deletion while an active session is using that key, the deletion might complete after authentication but before authorization:

// Revocation race condition
app.use('/api/*', async (req, res, next) => {
    const key = req.headers['x-api-key'];
    const user = await db.authenticate(key);
    if (!user) return res.status(401).send('Invalid key');
    
    // Race window: key might be revoked here
    const isRevoked = await db.isKeyRevoked(key);
    if (isRevoked) return res.status(403).send('Key revoked');
    
    req.user = user;
    next();
});

The window between authentication and revocation check allows attackers to use keys that should have been immediately invalidated.

API Keys-Specific Detection

Detecting race conditions in API key systems requires both static analysis and runtime monitoring. Static analysis tools can identify non-atomic operations that manipulate key state, while runtime monitoring catches actual race condition exploitation.

Code review should flag these anti-patterns:

// Anti-patterns to flag during code review
// 1. Separate read-modify-write operations
const key = await db.getApiKey(keyId);
// ... other operations ...
await db.updateApiKey(keyId, { revoked: true });

// 2. Multiple state checks without atomic operations
const isValid = await db.keyIsValid(key);
const isRevoked = await db.keyIsRevoked(key);
if (isValid && !isRevoked) { /* vulnerable */ }

// 3. Rate limiting without atomic increments
const count = await db.getRateCount(key);
if (count < limit) {
    await db.incrementRateCount(key);
    // Race condition here
}

Runtime detection focuses on anomalous patterns that suggest race condition exploitation. Monitor for:

  • Sudden spikes in successful authentications with recently revoked keys
  • Rate limit bypasses where request counts exceed configured thresholds
  • Multiple rapid key rotation events
  • Authentication failures immediately followed by successes with same key

middleBrick's black-box scanning approach detects race condition vulnerabilities by simulating concurrent operations. The scanner tests API key endpoints with parallel requests to identify timing gaps where authentication and authorization checks can be bypassed.

For comprehensive API key security assessment, middleBrick analyzes the unauthenticated attack surface across all 12 security categories, including authentication bypass attempts that race conditions enable. The scanner's property authorization checks specifically look for scenarios where key state changes don't propagate immediately to all system components.

middleBrick's continuous monitoring (Pro plan) can alert you when race condition patterns emerge in production, such as unusual authentication success rates immediately after key revocations or rotations.

API Keys-Specific Remediation

Remediating race conditions in API key systems requires atomic operations and proper synchronization. The most robust approach uses database transactions with row-level locks to ensure state changes are immediately visible to all concurrent operations.

For key rotation, use atomic compare-and-swap operations:

// Atomic key rotation using transactions
async function rotateApiKeyAtomic(userId, newKeyHash) {
    return await db.transaction(async (trx) => {
        // Lock the user's key row
        const keyRow = await trx('api_keys')
            .where('user_id', userId)
            .forUpdate()
            .first();
        
        // Atomically revoke old and activate new
        await trx('api_keys')
            .where('id', keyRow.id)
            .update({ revoked: true, rotated_at: new Date() });
            
        await trx('api_keys').insert({
            user_id: userId,
            key_hash: newKeyHash,
            created_at: new Date()
        });
    });
}

This ensures no window exists where both keys are invalid or where the old key remains active after rotation.

Rate limiting requires atomic increment operations with compare-and-swap semantics:

// Atomic rate limiting
async function checkRateLimitAtomic(apiKey) {
    return await db.transaction(async (trx) => {
        const row = await trx('rate_limits')
            .where('api_key', apiKey)
            .forUpdate()
            .first();
            
        if (!row) {
            await trx('rate_limits').insert({
                api_key: apiKey,
                count: 1,
                window_start: new Date()
            });
            return true;
        }
        
        const now = new Date();
        const windowSize = 60 * 1000; // 1 minute
        
        if (now - row.window_start > windowSize) {
            // Reset window
            await trx('rate_limits')
                .where('id', row.id)
                .update({ count: 1, window_start: now });
            return true;
        }
        
        if (row.count >= MAX_REQUESTS) {
            return false;
        }
        
        // Atomic increment
        await trx('rate_limits')
            .where('id', row.id)
            .update('count', trx.raw('count + 1'));
            
        return true;
    });
}

For distributed systems, use advisory locks or distributed locking mechanisms like Redis with SET NX operations to coordinate key state changes across multiple service instances.

Implement idempotent operations where possible. For key revocation, use a status field with atomic updates rather than delete operations:

// Idempotent revocation
async function revokeKeyAtomic(keyId) {
    return await db('api_keys')
        .where('id', keyId)
        .update('status', 'REVOKED');
}

This prevents race conditions where a key might be recreated between delete and recreation attempts.

Consider using compare-and-swap (CAS) operations if your database supports them. PostgreSQL's UPDATE ... WHERE conditions provide CAS semantics:

// CAS pattern for key state changes
async function updateKeyStatus(keyId, expectedStatus, newStatus) {
    const result = await db('api_keys')
        .where({ id: keyId, status: expectedStatus })
        .update({ status: newStatus });
        
    return result > 0; // true if update succeeded
}

This ensures state transitions only occur when the key is in the expected state, preventing race conditions from causing inconsistent states.

Frequently Asked Questions

How can I test for race conditions in my API key system?
Use middleBrick's black-box scanning to simulate concurrent API key operations. The scanner tests authentication endpoints with parallel requests to identify timing gaps where race conditions can be exploited. For manual testing, use load testing tools like k6 or artillery to send hundreds of simultaneous requests to key validation endpoints, looking for inconsistent responses or rate limit bypasses.
Do race conditions only affect high-traffic APIs?
No. While high traffic increases the likelihood of race condition exploitation, even low-traffic APIs can be vulnerable. An attacker can deliberately trigger race conditions using multiple threads or processes. The critical factor is whether your API key operations are atomic and properly synchronized, not the volume of legitimate traffic.