A == comparison on a token looks harmless. In practice, it leaks a measurable piece of information: execution time varies depending on how many characters match. That is the principle behind a timing attack, and it is enough for an attacker to reconstruct the token one character at a time.
The problem: the comparison that stops too early
Python compares strings character by character and stops as soon as a mismatch is found.
def verify_token(request):
user_token = request.GET.get('token')
valid_token = "secret_abc123"
if user_token == valid_token:
return JsonResponse({'access': 'granted'})
return JsonResponse({'access': 'denied'})
In practice:
user_token = "x"fails at the first character, returns almost instantly.user_token = "secret_ab"fails at the 10th character, returns slightly later.
The difference is in the nanosecond range, but it becomes measurable with enough requests. An attacker automates thousands of calls, measures response times, and deduces the token character by character. This is a timing attack, a side-channel attack that exploits execution time variations.
The fix: constant_time_compare()
Django provides constant_time_compare() in django.utils.crypto. This function compares two strings in constant time: the result takes exactly the same amount of time to compute regardless of how similar the two values are.
from django.utils.crypto import constant_time_compare
def verify_token(request):
user_token = request.GET.get('token', '')
valid_token = "secret_abc123"
if constant_time_compare(user_token, valid_token):
return JsonResponse({'access': 'granted'})
return JsonResponse({'access': 'denied'})
Two important details in this fix:
request.GET.get('token', '')instead ofget('token'): prevents aNonevalue from raising an exception during the comparison.valid_tokenmust never be hardcoded in production. It should come from an environment variable or a secret manager.
Under the hood: hmac.compare_digest
constant_time_compare() is a wrapper around hmac.compare_digest() from the Python standard library. This function guarantees identical execution time by comparing bytes without short-circuiting.
import hmac
# Direct equivalent outside Django
if hmac.compare_digest(user_token, valid_token):
...
In a Django project, constant_time_compare() is preferred because it normalizes input types and handles edge cases such as Unicode string comparison.
Other use cases
This protection applies to any context where a secret comparison is involved.
Webhook signature verification:
from django.conf import settings
from django.utils.crypto import constant_time_compare
import hashlib
import hmac
def verify_webhook(request):
payload = request.body
received_sig = request.headers.get('X-Hub-Signature-256', '')
expected_sig = 'sha256=' + hmac.new(
settings.WEBHOOK_SECRET.encode(),
payload,
hashlib.sha256
).hexdigest()
if not constant_time_compare(received_sig, expected_sig):
return HttpResponse(status=403)
# process the webhook
API key verification:
from django.utils.crypto import constant_time_compare
def api_key_middleware(get_response):
def middleware(request):
provided_key = request.headers.get('X-Api-Key', '')
if not constant_time_compare(provided_key, settings.API_SECRET_KEY):
return JsonResponse({'error': 'unauthorized'}, status=401)
return get_response(request)
return middleware
When this risk is real
| Context | Risk level |
|---|---|
| Internal API, local network | ⚠️ High: low latency, precise measurements |
| Public API over the internet | ⚠️ Moderate: feasible with enough requests |
| Non-confidential value (slug, status, public ID) | ✅ No risk: == is fine |
The decision rule is simple: whenever a comparison involves a secret (token, signature, API key), use constant_time_compare(). The performance cost is negligible, the protection is real.
