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 de get('token'): evita que un valor None lance una excepción durante la comparación.
  • valid_token no 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

ContextoNivel 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.