Rate Limiting Bypass in Django with Cockroachdb
Rate Limiting Bypass in Django with Cockroachdb — how this specific combination creates or exposes the vulnerability
Django’s rate-limiting facilities rely on a backend to store counters and timestamps. When CockroachDB is used as that backend, several implementation details can weaken the intended throttling behavior and enable a Rate Limiting Bypass. Unlike single-node databases, CockroachDB’s distributed, strongly consistent architecture changes timing and transaction semantics in ways that can be abused by an attacker who understands isolation levels and retry behavior.
One common pattern is to store per-identifier counters in a table with a row per client or API key. If the update logic uses a non-atomic read-modify-write outside a properly isolated transaction, an attacker can race concurrent requests to avoid incrementing the counter correctly. CockroachDB’s default SERIALIZABLE isolation prevents write skew, but if the application uses REPEATABLE READ or weaker, or inadvertently allows autocommit to split steps, two concurrent requests may both read the same count, increment locally, and commit — effectively adding only one increment instead of two. This allows an attacker to send many requests in parallel and stay under the configured limit.
Another bypass vector is timestamp and clock handling. Django’s cache- or db-based rate limiters often check last access time using NOW() or timezone-aware datetimes. CockroachDB nodes are distributed and maintain HLC (Hybrid Logical Clocks), which are consistent but may expose tiny divergences in reported NOW() across nodes. An attacker who can route requests to different nodes might manipulate perceived timing to reset windows or avoid lock contention checks that rely on monotonic time assumptions. If the application stores timestamps as TIMESTAMPTZ and compares them with Python’s datetime without strict timezone normalization, mismatches can cause the limiter to treat a stale window as valid.
Schema design also contributes. If the rate-limit table lacks a properly declared unique constraint on the identifier (e.g., client IP or API key), Django’s get_or_create or update_or_create can create multiple rows for the same identifier under high concurrency. Each row carries its own counter, so the attacker can cycle through identifiers or exploit the creation path to avoid incrementing a single shared counter. Without a database-level constraint, the integrity check happens too late, and the limiter silently aggregates across duplicates, effectively raising the allowed rate.
Moreover, CockroachDB’s linearizable reads depend on proper use of AS OF SYSTEM TIME and staleness preferences. If the Django app uses historical reads or low-staleness preferences to reduce latency, it may observe slightly stale counter values. An attacker can exploit this lag to perform bursts that appear within limits to the reading node but exceed limits globally. MiddleBrick’s checks for Rate Limiting include detecting weak isolation usage and missing unique constraints in the schema; scanning a Django endpoint backed by CockroachDB will surface these misconfigurations as high-severity findings tied to inconsistent state visibility and transaction anomalies.
Cockroachdb-Specific Remediation in Django — concrete code fixes
To close the bypass, enforce atomic increments and strict uniqueness in the database layer, and align time handling across nodes. Below are concrete patterns and CockroachDB-specific code examples for Django models and queries.
1. Enforce uniqueness with a database constraint
Ensure only one row per identifier exists. Add a UNIQUE constraint and use Django’s on_conflict_do_nothing to avoid race conditions during creation.
from django.db import models
class RateLimit(models.Model):
identifier = models.CharField(max_length=255) # e.g., API key or IP
count = models.PositiveIntegerField(default=0)
last_access = models.DateTimeField(auto_now=True)
class Meta:
constraints = [
models.UniqueConstraint(fields=['identifier'], name='unique_identifier')
]
2. Atomic increment in a serializable transaction
Use select_for_update inside a transaction to lock the row, or use an atomic F() expression with a conditional update to avoid read-modify-write races. The F() approach avoids explicit locking and leverages CockroachDB’s serializable isolation to ensure correctness under concurrency.
from django.db import transaction
from django.db.models import F
from django.utils import timezone
def increment_counter(identifier: str, window_seconds: int = 60):
with transaction.atomic():
# Optimistic update: add 1 only if the row belongs to the current window
updated = RateLimit.objects.filter(
identifier=identifier,
last_access__gte=timezone.now() - timezone.timedelta(seconds=window_seconds)
).update(count=F('count') + 1)
if updated == 0:
# No eligible row; create one safely with on_conflict_do_nothing
RateLimit.objects.update_or_create(
identifier=identifier,
defaults={'count': 1, 'last_access': timezone.now()}
)
return
3. Use upsert with conditional check to avoid duplicates
Leverage PostgreSQL-style upsert syntax via Django’s ORM (requires Django 3.1+ and CockroachDB compatibility). This avoids TOCTOU between get and create.
from django.db import IntegrityError
def safe_increment_or_create(identifier: str):
try:
RateLimit.objects.insert(
columns=['identifier', 'count', 'last_access'],
values=[identifier, 1, timezone.now()],
on_conflict_do_update={
'fields': ['identifier'],
'update': {'count': F('count') + 1}
}
)
except IntegrityError:
# Fallback for older Django or specific driver constraints
with transaction.atomic():
obj, created = RateLimit.objects.get_or_create(
identifier=identifier,
defaults={'count': 1, 'last_access': timezone.now()}
)
if not created:
obj.count = F('count') + 1
obj.save(update_fields=['count', 'last_access'])
4. Ensure timezone-aware comparisons
Always store and compare datetimes as UTC-aware TIMESTAMPTZ. Normalize inputs in Python and avoid mixing naive datetimes to prevent window misalignment across CockroachDB nodes.
from django.utils.timezone import make_aware, utc
from datetime import datetime
def normalize_for_window(ts):
if ts.tzinfo is None:
return make_aware(ts, utc)
return ts.astimezone(utc)
5. Validation and monitoring
After applying the above, verify uniqueness and monotonicity by running a lightweight check during deployment or via a management command. Use EXPLAIN ANALYZE on the update path to confirm serializable execution plans and ensure no fallback to two-phase commits that could expose stale reads.
These changes align with the checks that middleBrick performs for Rate Limiting, and they reduce the risk of bypass via race conditions or timestamp ambiguities when Django operates on CockroachDB.
Related CWEs: resourceConsumption
| CWE ID | Name | Severity |
|---|---|---|
| CWE-400 | Uncontrolled Resource Consumption | HIGH |
| CWE-770 | Allocation of Resources Without Limits | MEDIUM |
| CWE-799 | Improper Control of Interaction Frequency | MEDIUM |
| CWE-835 | Infinite Loop | HIGH |
| CWE-1050 | Excessive Platform Resource Consumption | MEDIUM |