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:
-
__init__(self, get_response)- Inicializa la instancia.
- Lee
settings.RATE_LIMIT_ENDPOINTSenself.custom_limits. - Lee
settings.RATE_LIMIT_ENABLED(default:True).
-
__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á enTENANT_EXEMPT_PATHSy el usuario tiene sesión, aplica también el límite por plan de la organización. - Si OK: pasa al siguiente middleware/view.
-
get_limit_for_path(self, path)- Busca match exacto en
self.custom_limits(settings) →STRICT_ENDPOINTS→None(sin límite).
- Busca match exacto en
-
check_rate_limit(self, ip, path, limit)- Clave Redis
ratelimit:{path}:{ip}, contador deslizante dentro dewindowsegundos.
- Clave Redis
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
- Backend: Redis (
django_redis), distribuido entre procesos. - Fallback en tests:
LocMemCache(una por worker de pytest-xdist para evitar contadores compartidos).
Comportamiento de error
- Si Redis no está disponible: se registra el fallo y se deja pasar el request (fail-open para disponibilidad, pero se loguea).
- Si configuración corrupta: se registra y se trata como “sin límite” (fallback seguro).
Rutas protegidas (actualizado 24-09-2026)
| Endpoint | Límite | Propósito |
|---|---|---|
/api/signup | 3/min | Prevenir spam de registro de orgs |
/accounts/login/ | 5/min | Prevenir fuerza bruta en login |
/api/auth/passkey-options/ | 10/min | Validación biométrica |
/api/auth/validate-and-setup-biometric/ | 5/min | Setup biométrico |
/api/blueprints/auto-plan | 10/min | Generación IA (alto costo) |
/api/network/device/ | 30/min | Operaciones CRUD de dispositivos |
/api/metrics/web-vitals | 300/min | Telemetrí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)
- B-30:
get_client_ipdeja de ser un método de la clase con su propia copia; pasa a función de módulo compartida conCF-Connecting-IPcomo fuente primaria (ver [[feature—core—ip-real-cloudflare-v1-45-13]]). - B-39:
/api/metrics/web-vitalssale de la exención total (EXEMPT_PATHS), entra enSTRICT_ENDPOINTS(300/min por IP) y en la nuevaTENANT_EXEMPT_PATHS(no gasta cuota de organización). El path se normaliza a patrón de ruta antes de usarse como etiqueta de métrica — cierra el riesgo de cardinalidad sin límite en Prometheus.
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
- Sincronización: Todos los workers leen/escriben en la misma instancia Redis, garantizando coherencia global.
- 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).
- Headers HTTP 429: Incluyen
Retry-Afterpara que clientes inteligentes respeten el backoff. - Overhead: Una operación Redis por request en ruta protegida (bajo costo, <5ms típico).
Véase también
- [[feature—auth—cierre-signup-allauth]]
- [[entity—core—model—organization]]
- [[concept—saas—multi-tenancy]]
- [[entity—core—service—require-perm]]
- [[entity—core—module—admin-paths]]
- [[feature—core—ip-real-cloudflare-v1-45-13]]