# WAF: ModSecurity v3 + OWASP Core Rule Set (CRS)
## 1. Qué es y por qué se usa
Un **WAF (Web Application Firewall)** es un cortafuegos de aplicación que inspecciona el tráfico HTTP en busca de patrones maliciosos antes de que las peticiones lleguen al backend. A diferencia de un firewall de red (capas 3-4), un WAF opera en la capa 7 (aplicación) y puede analizar el contenido de las peticiones: cabeceras, query strings, cuerpos JSON/formulario, cookies, etc.
En esta aplicación se despliega **ModSecurity v3** (motor WAF open source, mantenido por Trustwave/OWASP) junto con el **OWASP Core Rule Set (CRS)**, el conjunto de reglas estándar de la industria que cubre las categorías de ataques más comunes (OWASP Top 10).
### Qué aporta a la aplicación
- **Defensa en profundidad**: las validaciones de entrada del backend (`app/helpers.py`) protegen contra datos malformados; el WAF actúa como **capa adicional** que bloquea ataques conocidos antes de que lleguen al código de la aplicación.
- **Protección contra ataques genéricos**: SQL injection, XSS, path traversal, command injection, scanners automatizados, etc.
- **Prevención de exfiltración de datos**: inspección de respuestas (outbound filtering) para bloquear fugas de información sensible (números de tarjeta, rutas del sistema, volcados de BD, stack traces).
- **Sin cambios en el código**: el WAF se despliega como reverse proxy delante del backend; la aplicación Flask no necesita modificaciones.
- **Ocultación del backend**: el usuario se conecta al WAF (Nginx); el backend Flask no es accesible directamente desde fuera de la red Docker.
---
## 2. Arquitectura
```
┌──────────────────────────────────────────────┐
│ Red Docker (proxy-network) │
│ │
Usuario :5001 ──▶ │ WAF (Nginx + ModSecurity + CRS) ──▶ Flask │
│ owasp/modsecurity-crs:nginx-alpine :5001 │
│ contenedor: waf_modsecurity web │
│ │
└──────────────────────────────────────────────┘
```
| Componente | Detalle |
|------------|---------|
| **Imagen Docker** | `owasp/modsecurity-crs:nginx-alpine` (imagen oficial OWASP) |
| **Contenedor** | `waf_modsecurity` |
| **Puerto expuesto** | `5001` (host) → `8080` (contenedor WAF) |
| **Backend** | `http://web:5001` (solo accesible dentro de la red Docker) |
| **Arranque** | Automático con `make` o `docker-compose up -d`. No requiere perfil. |
| **Dependencia** | `depends_on: web: condition: service_healthy` — el WAF espera a que Flask esté sano antes de aceptar tráfico. |
El servicio `web` (Flask/Gunicorn) usa `expose: "5001"` en lugar de `ports`, por lo que **no publica** su puerto en el host. Toda petición del usuario pasa obligatoriamente por el WAF.
---
## 3. Proceso de despliegue: DetectionOnly → On
El despliegue se realizó siguiendo la metodología recomendada en dos fases:
### Fase 1: Modo DetectionOnly (observación)
Se configuró `MODSEC_RULE_ENGINE=DetectionOnly` para que el WAF registrase las alertas en los logs **sin bloquear** ninguna petición:
```yaml
# docker-compose.yml → servicio waf → environment
- MODSEC_RULE_ENGINE=DetectionOnly
```
En esta fase se navegó por toda la aplicación (registro, login, perfil, pesos, supervisor) y se ejecutaron ataques de prueba. Los logs mostraron:
- Qué reglas se activaban con tráfico legítimo (falsos positivos).
- Qué ataques eran correctamente detectados (verdaderos positivos).
- El scoring de anomalía acumulado por cada petición.
**Resultado**: se identificaron 4 falsos positivos (sección 5) y se redactaron las exclusiones.
### Fase 2: Modo On (bloqueo)
Tras verificar que las exclusiones cubrían todos los falsos positivos, se cambió a modo bloqueo:
```yaml
- MODSEC_RULE_ENGINE=On
```
Se repitieron todas las pruebas: los ataques devolvían HTTP 403 y el tráfico legítimo pasaba sin problemas (HTTP 200).
---
## 4. Configuración
Las variables de entorno del contenedor WAF se definen en `docker-compose.yml` → servicio `waf`:
| Variable | Valor | Descripción |
|----------|-------|-------------|
| `BACKEND` | `http://web:5001` | URL interna del backend Flask al que el WAF reenvía las peticiones limpias |
| `MODSEC_RULE_ENGINE` | `On` | Modo de operación de ModSecurity: `On` bloquea; `DetectionOnly` solo registra |
| `PARANOIA` | `1` | Paranoia Level del OWASP CRS (ver sección siguiente) |
| `ANOMALY_INBOUND` | `5` | Umbral de anomalía para peticiones entrantes |
| `ANOMALY_OUTBOUND` | `4` | Umbral de anomalía para respuestas salientes |
| `ALLOWED_METHODS` | `GET HEAD POST OPTIONS PUT DELETE` | Métodos HTTP permitidos |
| `ALLOWED_REQUEST_CONTENT_TYPE` | `application/json`, `multipart/form-data`, etc. | Content-Types permitidos |
### Paranoia Level (PL)
| PL | Descripción | Falsos positivos | Uso recomendado |
|----|-------------|------------------|-----------------|
| **1** | Solo reglas de alta confianza. | Muy bajo | **Producción general** (valor actual) |
| 2 | Reglas adicionales de confianza media. | Bajo-medio | Datos sensibles con tiempo para ajustar |
| 3 | Reglas agresivas. | Medio-alto | Alta seguridad con exclusiones bien ajustadas |
| 4 | Máxima detección. | Muy alto | Solo pruebas o entornos muy controlados |
### Anomaly Scoring
ModSecurity CRS usa un modelo de **anomaly scoring**: cada regla que se activa suma puntos. La petición/respuesta se bloquea solo cuando el total acumulado alcanza el umbral.
- **Inbound (5)**: una regla crítica (SQLi, XSS) normalmente suma 5 puntos → bloqueo inmediato.
- **Outbound (4)**: protege contra fugas de información en las respuestas.
---
## 5. Análisis de falsos positivos y exclusiones
Fichero: **`waf/modsecurity-override.conf`**
Este fichero se monta como regla **AFTER-CRS** (`RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf`). Las exclusiones se aplican **después** de cargar todas las reglas del Core Rule Set, lo que permite modificar el comportamiento de reglas específicas sin alterar el CRS original.
### 5.1. Falso positivo 1: Campo `password` en login/register
**Detección en logs (modo DetectionOnly):**
```
ruleId: "942100"
file: "REQUEST-942-APPLICATION-ATTACK-SQLI.conf"
msg: "SQL Injection Attack Detected via libinjection"
data: "Matched Data: 1&1 found within ARGS:password"
severity: 2
Inbound Anomaly Score: 5
```
**Causa**: las contraseñas pueden contener caracteres como `'`, `"`, `<`, `>`, `--`, `OR`, etc. que disparan reglas de inyección SQL (942xxx) y XSS (941xxx).
**Exclusión (regla 10001)**:
```
SecRule REQUEST_URI "@beginsWith /api/auth/"
ctl:ruleRemoveTargetById=941100-941999;ARGS:password ← XSS
ctl:ruleRemoveTargetById=942100-942999;ARGS:password ← SQLi
ctl:ruleRemoveTargetById=920272;ARGS:password ← Tamaño
```
**Justificación**: la exclusión se aplica DESPUÉS del CRS (after-CRS) y es específica: solo afecta al campo `password` en las rutas `/api/auth/*`. El resto de campos y rutas siguen protegidos por las reglas completas. Se usa `ctl:ruleRemoveTargetById` para eliminar reglas concretas de un argumento concreto, no se desactiva ningún motor.
### 5.2. Falso positivo 2: Campo `recaptcha_token`
**Causa**: el token reCAPTCHA v3 es una cadena base64 larga que contiene secuencias que coinciden con patrones de inyección.
**Exclusión (regla 10002)**: elimina reglas 941xxx y 942xxx solo para `ARGS:recaptcha_token` en `/api/auth/*`.
### 5.3. Falso positivo 3: Campos `first_name` / `last_name`
**Causa**: nombres con apóstrofes (O'Brien), acentos, guiones compuestos activan reglas de inyección.
**Exclusión (regla 10003)**: elimina reglas 941xxx y 942xxx solo para `ARGS:first_name` y `ARGS:last_name` en `/api/user`.
### 5.4. Falso positivo 4: Dashboard Supervisor
**Causa**: el supervisor es un dashboard de desarrollo que muestra estadísticas de tráfico, consultas y datos del sistema. Su contenido HTML/JS activa múltiples familias de reglas.
**Exclusión (reglas 10004 + 10005)**: se eliminan **familias de reglas por ID específico** (NO se desactiva el motor del WAF):
```
SecRule REQUEST_URI "@beginsWith /supervisor"
ctl:ruleRemoveById=941100-941999 ← XSS (HTML/JS del dashboard)
ctl:ruleRemoveById=942100-942999 ← SQLi (datos SQL en la interfaz)
ctl:ruleRemoveById=951100-951999 ← SQL info leakage (outbound)
ctl:ruleRemoveById=952100-952999 ← Data leakage (rutas de ficheros)
ctl:ruleRemoveById=953100-953999 ← PHP/Java info leakage
ctl:ruleRemoveById=954100-954999 ← Application errors
ctl:ruleRemoveById=10010-10013 ← Reglas custom de exfiltración
```
**Justificación**: en producción, el supervisor debe estar desactivado (`APP_SUPERVISOR=0`), por lo que esta exclusión no aplica. Se usa `ctl:ruleRemoveById` para eliminar familias de reglas concretas en lugar de `ctl:ruleEngine=Off`, cumpliendo el requisito de no desactivar el motor del WAF por completo.
---
## 6. Protección contra exfiltración de datos (Outbound Filtering)
### Configuración
El WAF inspecciona el cuerpo de las respuestas para detectar fugas de información sensible:
```
SecResponseBodyAccess On
SecResponseBodyMimeType text/plain text/html text/xml application/json
SecResponseBodyLimit 524288
```
- **`SecResponseBodyAccess On`**: activa la inspección de respuestas.
- **`SecResponseBodyMimeType`**: tipos MIME inspeccionados (incluye JSON para la API REST).
- **`SecResponseBodyLimit`**: límite de 512 KB por respuesta para no impactar excesivamente el rendimiento.
### Reglas CRS de respuesta (automáticas)
Con el outbound filtering activado, las reglas CRS de las familias 950xxx-954xxx se activan automáticamente:
| Familia | Descripción |
|---------|-------------|
| 951xxx | SQL Information Leakage (errores SQL en respuestas) |
| 952xxx | Data Leakage (listados de directorios, rutas de ficheros) |
| 953xxx | PHP/Java Information Leakage |
| 954xxx | Application Error Messages (stack traces genéricos) |
### Reglas personalizadas de exfiltración
Se han añadido 4 reglas personalizadas que complementan las reglas CRS:
| ID | Detección | Ejemplo de patrón |
|----|-----------|-------------------|
| **10010** | Números de tarjeta de crédito (Visa, MasterCard, AMEX, Discover) | `4539578763621486` |
| **10011** | Contenido de `/etc/passwd` | `root:x:0:0:root:/root:/bin/bash` |
| **10012** | Volcados de base de datos | `CREATE TABLE`, `INSERT INTO`, `mysqldump` |
| **10013** | Stack traces de Python/Flask | `Traceback (most recent call last)`, `Debugger PIN` |
### Pruebas de exfiltración realizadas
Se crearon endpoints de test (`/test/exfiltration/*`, solo disponibles en modo desarrollo) que simulan fugas de datos. El WAF bloquea correctamente todas:
| Endpoint | Regla activada | Resultado en log |
|----------|---------------|------------------|
| `/test/exfiltration/passwd` | 10011 | `Access denied with code 403 (phase 4). msg: 'Posible fuga de contenido de /etc/passwd en respuesta'` |
| `/test/exfiltration/creditcard` | 10010 | `Access denied with code 403 (phase 4). msg: 'Posible fuga de número de tarjeta de crédito en respuesta'` |
| `/test/exfiltration/sqldump` | 10012 | `Access denied with code 403 (phase 4). msg: 'Posible volcado de base de datos en respuesta'` |
| `/test/exfiltration/stacktrace` | 10013 | `Access denied with code 403 (phase 4). msg: 'Posible fuga de stack trace o debugger de Python en respuesta'` |
**Extracto real del log (tarjeta de crédito bloqueada):**
```json
{
"transaction": {
"request": {"method": "GET", "uri": "/test/exfiltration/creditcard"},
"response": {"http_code": 403},
"producer": {"secrules_engine": "Enabled"},
"messages": [{
"message": "Posible fuga de número de tarjeta de crédito en respuesta",
"details": {
"ruleId": "10010",
"match": "Matched Operator Rx against variable RESPONSE_BODY",
"data": "4539578763621486",
"severity": "2",
"tags": ["DATA_LEAKAGE/CREDIT_CARD"]
}
}]
}
}
```
### Nota sobre el comportamiento en reverse proxy
Cuando una regla de fase 4 (outbound) se activa, ModSecurity intenta devolver un 403 al cliente. En modo reverse proxy, los headers HTTP pueden haberse enviado ya al cliente antes de que se inspeccione el body. En ese caso:
- El log registra `header already sent` como advertencia.
- ModSecurity **cierra la conexión**, truncando la respuesta.
- El cliente recibe una respuesta incompleta o un error de conexión, impidiendo la exfiltración completa.
---
## 7. Ataques bloqueados (batería de pruebas verificada)
### Extracto real del log — SQL Injection detectada (modo DetectionOnly)
```json
{
"transaction": {
"request": {
"method": "GET",
"uri": "/?id=1%20OR%201=1"
},
"producer": {
"modsecurity": "ModSecurity v3.0.14 (Linux)",
"secrules_engine": "DetectionOnly",
"components": ["OWASP_CRS/4.23.0"]
},
"messages": [{
"message": "SQL Injection Attack Detected via libinjection",
"details": {
"ruleId": "942100",
"file": "REQUEST-942-APPLICATION-ATTACK-SQLI.conf",
"data": "Matched Data: 1&1 found within ARGS:id: 1 OR 1=1",
"severity": "2",
"tags": ["attack-sqli", "paranoia-level/1", "OWASP_CRS"]
}
}, {
"message": "Inbound Anomaly Score Exceeded (Total Score: 5)",
"details": {
"ruleId": "949110",
"data": "TX:BLOCKING_INBOUND_ANOMALY_SCORE = 5"
}
}]
}
}
```
### Extracto real del log — XSS detectada (modo DetectionOnly)
```json
{
"transaction": {
"request": {
"method": "GET",
"uri": "/?x="
},
"messages": [
{"message": "XSS Attack Detected via libinjection", "details": {"ruleId": "941100"}},
{"message": "XSS Filter - Category 1: Script Tag Vector", "details": {"ruleId": "941110"}},
{"message": "NoScript XSS InjectionChecker: HTML Injection", "details": {"ruleId": "941160"}},
{"message": "Javascript method detected", "details": {"ruleId": "941390"}},
{"message": "Inbound Anomaly Score Exceeded (Total Score: 20)", "details": {"ruleId": "949110"}}
]
}
}
```
### Extracto real del log — Command Injection / LFI (modo DetectionOnly)
```json
{
"transaction": {
"request": {
"method": "GET",
"uri": "/?cmd=;cat%20/etc/passwd"
},
"messages": [
{"message": "OS File Access Attempt", "details": {"ruleId": "930120", "data": "etc/passwd found within ARGS:cmd"}},
{"message": "Remote Command Execution: Unix Shell Code Found", "details": {"ruleId": "932160"}},
{"message": "Inbound Anomaly Score Exceeded (Total Score: 10)", "details": {"ruleId": "949110"}}
]
}
}
```
### Extracto real del log — Scanner detectado (modo DetectionOnly)
```json
{
"transaction": {
"request": {
"method": "GET", "uri": "/",
"headers": {"User-Agent": "Nikto"}
},
"messages": [{
"message": "Found User-Agent associated with security scanner",
"details": {
"ruleId": "913100",
"data": "Matched Data: nikto found within REQUEST_HEADERS:User-Agent: Nikto"
}
}]
}
}
```
### Tabla resumen — Modo On (bloqueo activo)
#### Inyección SQL
| Payload | Vector | HTTP | Estado |
|---------|--------|------|--------|
| `?id=1 OR 1=1` | Query string | 403 | Bloqueado |
| `?q=1 UNION SELECT username,password FROM users` | Query string | 403 | Bloqueado |
#### Cross-Site Scripting (XSS)
| Payload | Vector | HTTP | Estado |
|---------|--------|------|--------|
| `` | Query string | 403 | Bloqueado |
| `` | Query string | 403 | Bloqueado |
| `