Race Condition in Chi with Api Keys
Race Condition in Chi with Api Keys — how this specific combination creates or exposes the vulnerability
A race condition in a Chi-based API occurs when multiple concurrent requests read and write shared state in an unsafe way, and the presence or misuse of API keys can exacerbate the problem. For example, an endpoint that performs a read-modify-write sequence on a keyed resource (such as a quota counter or a per-key configuration) without proper synchronization may allow two requests with different API keys to interfere with each other’s state transitions. If one request validates a key and checks a remaining quota, then a second request does the same before the first request updates the quota, both may pass validation and proceed to consume more than the allowed limit. This is a classic time-of-check-to-time-of-use (TOCTOU) race condition, where the window between check and update is exploitable.
Chi routes are often composed of middleware that can inspect or modify requests and responses. When API keys are validated in middleware or handlers, and the handler performs non-atomic operations against shared storage (e.g., a database row for a key’s remaining calls), concurrent requests with different keys can lead to inconsistent states. For instance, if key A and key B both hit an endpoint that decrements a global rate-limit counter in a non-atomic fashion, the counter may underflow or allow bursts beyond intended limits. In distributed deployments, race conditions can be more severe because key validation and state updates may happen on different nodes with stale caches, making consistency harder to achieve without coordination mechanisms like distributed locks or transactional writes.
An attacker with low-privilege API keys can exploit timing differences to probe for these conditions: send rapid parallel requests with slightly varied parameters to observe inconsistent authorization or quota enforcement. If the API key validation logic is not idempotent or does not use monotonic counters/versioning, the race may lead to privilege escalation (e.g., a lower-privilege key exceeding elevated limits) or data exposure (e.g., reading another key’s configuration due to shared mutable state). Because Chi does not inherently serialize these operations, developers must ensure that any shared mutable state associated with API keys is accessed atomically and that validation logic is designed to be resilient to concurrent execution.
Api Keys-Specific Remediation in Chi — concrete code fixes
To mitigate race conditions involving API keys in Chi, ensure that operations that read and update shared key state are performed atomically and that validation logic avoids unsafe check-then-act patterns. Prefer database-level constraints (unique indexes, transactions with appropriate isolation levels) or atomic increment/decrement operations. Below are concrete code examples using Chi and a PostgreSQL-backed key store to illustrate safe handling.
// models/key.dart
class ApiKey {
final String id;
final String key; // stored hashed
final int quotaLimit;
int usedCount;
final DateTime expiresAt;
ApiKey({
required this.id,
required this.key,
required this.quotaLimit,
required this.usedCount,
required this.expiresAt,
});
bool get isActive => DateTime.now().isBefore(expiresAt) && usedCount < quotaLimit;
}
Use a repository that performs quota checks and increments atomically on the database side. For PostgreSQL, use an upsert with a WHERE condition that enforces the quota, so the update fails if the limit would be exceeded:
// repository/key_repository.dart
import 'package:postgres/postgres.dart';
class KeyRepository {
final PostgreSQLConnection connection;
KeyRepository(this.connection);
/// Attempts to consume one unit for [keyHash] if quota allows.
/// Returns true if the consumption succeeded, false otherwise.
Future tryConsumeKey(String keyHash) async {
const query = '''
UPDATE api_keys
SET used_count = used_count + 1
WHERE key_hash = @keyHash
AND used_count + 1 <= quota_limit
AND expires_at > NOW()
RETURNING id;
''';
final result = await connection.execute(
QueryExecutor.simple(query, substitutionValues: {'keyHash': keyHash}),
);
return result.isNotEmpty;
}
}
In your Chi handler, call this atomic repository method and reject the request if the update fails. This removes the window for a race because the database enforces the constraint under concurrency:
// routes/key_route.dart
import 'package:chi/chi.dart';
import 'package:clock/clock.dart';
void setupKeyRoute(Chi chi, KeyRepository keyRepo) {
chi.post('/consume', (req) async {
final apiKey = req.context['apiKey'] as String; // already extracted by middleware
final keyHash = hashKey(apiKey); // e.g., SHA-256
final ok = await keyRepo.tryConsumeKey(keyHash);
if (!ok) {
return Response.forbidden(body: 'Quota exceeded or key invalid');
}
return Response.ok(body: 'Allowed');
});
}
For middleware that resolves the API key, perform only lightweight lookups (e.g., fetching metadata) and avoid mutating shared state. Store the resolved key info in the request context for downstream handlers. Ensure that any writes (like incrementing usage counters) are handled by the atomic repository shown above. If you use in-memory caches for performance, treat them as best-effort and always enforce limits in the authoritative data store to avoid race conditions caused by cache staleness.