Volver a la wiki

Middleware RateLimitMiddleware — Control anti-abuso por endpoint

Descripción

Middleware Django que implementa rate limiting con ventana deslizante (sliding window) sobre endpoints sensibles. Protege contra ataques de fuerza bruta en login, registro, autenticación biométrica y endpoints de alto costo computacional (ej. generación de planos de IA), y limita la telemetría anónima (Web Vitals).

Ubicación

core/middleware/rate_limit.py

Configuración

Los límites se definen en dos niveles jerárquicos (settings > middleware > defaults):

1. settings.RATE_LIMIT_ENDPOINTS (override principal)

En config/settings/base.py:

RATE_LIMIT_ENDPOINTS = {
    "/api/signup": {"requests": 3, "window": 60},  # 3/min — registro de org
    "/accounts/login/": {"requests": 5, "window": 60},  # 5/min — login form
    "/api/auth/passkey-options/": {"requests": 10, "window": 60},
    "/api/auth/validate-and-setup-biometric/": {"requests": 5, "window": 60},
    "/api/blueprints/auto-plan": {"requests": 10, "window": 60},  # AI
    "/api/network/device/": {"requests": 30, "window": 60},
    "/api/metrics/web-vitals": {"requests": 300, "window": 60},  # v1.156.0, B-39
}

En config/settings/production.py: override más estrictos si aplica.

2. RateLimitMiddleware.STRICT_ENDPOINTS (fallback si no en settings)

Mismo contenido que arriba, definido como fallback de clase.

Componentes

get_client_ip(request, trusted_proxy_nets=None) — función de módulo (v1.156.0)

Antes vivía como método RateLimitMiddleware.get_client_ip con su propia copia de la lógica de extracción; desde la mega-auditoría B-30 (24-09-2026) es una función de módulo compartida por 4 sitios (este middleware, impersonate_start, core/auth_api.py, core/signals.py). El método de la clase pasa a ser un wrapper de una línea. Detalle completo de la lógica (CF-Connecting-IP primero, XFF de respaldo, solo si el salto directo es un proxy de confianza) en [[feature—core—ip-real-cloudflare-v1-45-13]].

RateLimitMiddleware (clase principal)

Firma: class RateLimitMiddleware

Métodos públicos:

  1. __init__(self, get_response)

    • Inicializa la instancia.
    • Lee settings.RATE_LIMIT_ENDPOINTS en self.custom_limits.
    • Lee settings.RATE_LIMIT_ENABLED (default: True).
  2. __call__(self, request)

    • Procesa cada request HTTP.
    • Extrae IP origen vía get_client_ip (función de módulo, ver arriba).
    • Capa 1 (por IP): obtiene límite para el path, aplica sliding window en Redis, 429 si excede.
    • Capa 2 (por organización, v1.156.0 con exención): si path.startswith("/api/") y NO está en TENANT_EXEMPT_PATHS y el usuario tiene sesión, aplica también el límite por plan de la organización.
    • Si OK: pasa al siguiente middleware/view.
  3. get_limit_for_path(self, path)

    • Busca match exacto en self.custom_limits (settings) → STRICT_ENDPOINTS → None (sin límite).
  4. check_rate_limit(self, ip, path, limit)

    • Clave Redis ratelimit:{path}:{ip}, contador deslizante dentro de window segundos.

TENANT_EXEMPT_PATHS (v1.156.0, mega-auditoría B-39)

TENANT_EXEMPT_PATHS = ("/api/metrics/web-vitals",)

Rutas de /api/ que NO gastan la cuota por organización (Capa 2). Hasta v1.156.0, /api/metrics/ estaba en EXEMPT_PATHS (fuera de TODO límite, ni siquiera por IP) — el endpoint anónimo /api/metrics/web-vitals guardaba el path crudo del request (recortado a 100 caracteres) como etiqueta de Prometheus sin ningún tope, así que un script podía inflar la memoria del proceso creando series nuevas sin parar. Ahora: core/api/web_vitals.py::normalize_path reduce el path al patrón de ruta de Django (racks/<int:pk>/) u "other" antes de usarlo como etiqueta, el endpoint SALE de EXEMPT_PATHS y entra en STRICT_ENDPOINTS con 300/min por IP (UNVERIFIED: criterio, no medido contra tráfico real), y entra en TENANT_EXEMPT_PATHS para que los ~5 envíos por página vista (llegan con cookie de sesión) no le quiten cuota a las llamadas reales de la organización (60-120/min según plan).

Almacenamiento

Comportamiento de error

Rutas protegidas (actualizado 24-09-2026)

EndpointLímitePropósito
/api/signup3/minPrevenir spam de registro de orgs
/accounts/login/5/minPrevenir fuerza bruta en login
/api/auth/passkey-options/10/minValidación biométrica
/api/auth/validate-and-setup-biometric/5/minSetup biométrico
/api/blueprints/auto-plan10/minGeneración IA (alto costo)
/api/network/device/30/minOperaciones CRUD de dispositivos
/api/metrics/web-vitals300/minTelemetría RUM anónima (v1.156.0, antes sin límite)

Historial de cambios

2026-08-04: Corrección de rutas (#359)

Antes: Protegía /api/auth/login y /api/auth/register — rutas que nunca existieron en el codebase. Ahora: rutas fantasma eliminadas, /accounts/login/ y /api/signup (rutas reales) añadidas.

2026-09-24: mega-auditoría B-30 + B-39 (v1.156.0, PR #603, commit 5d06aef1)

Límite honesto (B-39): el panel de Web Vitals pierde el detalle del path crudo (ahora ve el patrón, no la URL exacta); los 300/min son de criterio, no medidos contra tráfico real.

Testing

Fichero: tests/api/test_signup_gate.py — rutas de signup/login protegidas y ausencia de rutas fantasma. tests/qa/test_mega_T10.py y el bloque B-39 de la mega-auditoría cubren el nuevo límite de Web Vitals (verificar fichero exacto en el PR #603 si se profundiza).

Notas de implementación

  1. Sincronización: Todos los workers leen/escriben en la misma instancia Redis, garantizando coherencia global.
  2. Pérdida de requests en transición: Si Redis se reinicia, los contadores se pierden (ventana nueva), pero es seguro (puede haber spike momentáneo, no al revés).
  3. Headers HTTP 429: Incluyen Retry-After para que clientes inteligentes respeten el backoff.
  4. Overhead: Una operación Redis por request en ruta protegida (bajo costo, <5ms típico).

Véase también

Subir