Una comparación == sobre un token parece inofensiva. En la práctica, filtra información medible: el tiempo de ejecución varía según cuántos caracteres coinciden. Ese es el principio de una timing attack, y basta para que un atacante reconstruya el token carácter a carácter.
El problema: la comparación que se detiene demasiado pronto
Python compara cadenas carácter a carácter y se detiene en cuanto encuentra una diferencia.
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'})
En la práctica:
user_token = "x"falla en el primer carácter, retorno casi inmediato.user_token = "secret_ab"falla en el décimo carácter, retorno ligeramente más lento.
La diferencia es de nanosegundos, pero se vuelve medible con suficientes peticiones. Un atacante automatiza miles de llamadas, mide los tiempos de respuesta y deduce el token carácter a carácter. Es una timing attack, un ataque de canal lateral que explota las variaciones en el tiempo de ejecución.
La corrección: constant_time_compare()
Django proporciona constant_time_compare() en django.utils.crypto. Esta función compara dos cadenas en tiempo constante: el resultado tarda exactamente el mismo tiempo en calcularse independientemente de la similitud entre los valores.
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'})
Dos detalles importantes en esta corrección:
request.GET.get('token', '')en lugar deget('token'): evita que un valorNonelance una excepción durante la comparación.valid_tokenno debe codificarse directamente en producción. Debe provenir de una variable de entorno o de un gestor de secretos.
Bajo el capó: hmac.compare_digest
constant_time_compare() es un wrapper alrededor de hmac.compare_digest() de la biblioteca estándar de Python. Esta función garantiza un tiempo de ejecución idéntico al comparar bytes sin cortocircuito.
import hmac
# Equivalente directo fuera del contexto Django
if hmac.compare_digest(user_token, valid_token):
...
En un proyecto Django, constant_time_compare() es preferible porque normaliza los tipos de entrada y gestiona casos límite como la comparación de cadenas Unicode.
Otros casos de uso
Esta protección se aplica a cualquier contexto donde haya una comparación de secretos.
Verificación de firma de webhook:
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)
# procesar el webhook
Verificación de clave API:
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
Cuándo este riesgo es real
| Contexto | Nivel de riesgo |
|---|---|
| API interna, red local | ⚠️ Alto: baja latencia, mediciones precisas |
| API pública en internet | ⚠️ Moderado: viable con suficientes peticiones |
| Valor no confidencial (slug, estado, ID público) | ✅ Sin riesgo: == es suficiente |
La regla de decisión es sencilla: cuando una comparación involucra un secreto (token, firma, clave API), usar constant_time_compare(). El coste en rendimiento es despreciable, la protección es real.
