Nada arruina más un turno de guardia tranquilo que la captura de pantalla del CEO mostrando un “403 Forbidden” en la página de acceso de WordPress. El equipo de marketing no puede publicar. Los editores no pueden subir archivos. Los clientes no pueden pagar. Y tu WAF, que debería ser el adulto tranquilo en la sala, ahora es el portero echando a los invitados.
La solución no es “desactivar el WAF” ni “poner en lista blanca todo lo que esté bajo /wp-admin/”. La solución es obtener evidencia, aislar la regla exacta y ajustar la excepción más pequeña posible con protecciones. Quieres que WordPress funcione y que los atacantes se aburran. Ambas cosas son posibles.
Qué ocurre realmente cuando WordPress está “bloqueado por el WAF”
Cuando los usuarios dicen “el WAF bloqueó WordPress”, normalmente quieren decir una de cuatro cosas:
- Un positivo verdadero: alguien (o algo) está intentando realmente SQLi/XSS/RCE y el WAF lo bloqueó. WordPress es solo el punto donde aterrizó el tráfico.
- Un falso positivo: un comportamiento legítimo de WordPress parece sospechoso. Los habituales: HTML del editor, peticiones de la REST API, endpoints AJAX, subidas de archivos o páginas de administración de plugins con parámetros extraños.
- Un disparador de control de tasa/comportamiento: protección contra bots, protección de inicio de sesión o limitación genérica de tasa sobrecarga la actividad administrativa, trabajos cron o una comprobación de estado del CDN.
- Desajuste de límites de confianza: la aplicación ve IPs de cliente distintas a las que ve el WAF, o cookies/headers son modificados por un proxy. Una regla diseñada para proteger una capa ahora castiga otra.
La mayor parte del dolor WordPress/WAF surge por la concentración de endpoints. WordPress canaliza mucho comportamiento complejo a través de pocas URL:
/wp-login.phppara autenticación/wp-admin/admin-ajax.phppara comportamientos asincrónicos de la interfaz (frontend y backend)/wp-json/para la REST API (incluidas funciones de Gutenberg/editor)/xmlrpc.phphistóricamente usado para publicación remota; hoy mayormente usado por bots e integraciones antiguas/wp-admin/para todo lo demás que parece “administrativo” y por lo tanto “atacable”
Los WAFs no “entienden WordPress”. Entienden peticiones HTTP y patrones que a menudo se correlacionan con ataques. WordPress genera contenido y parámetros que accidentalmente semejan ataques. Tu trabajo es reducir el desajuste sin eliminar protección significativa.
Una idea orientadora, parafraseada de John Allspaw: la operación consiste en permitir cambios sin salir heridos. Eso también aplica a los WAF: las excepciones seguras son una forma de madurez operativa.
Guía rápida de diagnóstico (primero/segundo/tercero)
Primero: confirma que es el WAF (no la app, no el CDN, no la autenticación)
- Mira el estado HTTP: 403/406/429 son resultados comunes del WAF, pero las aplicaciones también devuelven 403.
- Busca cabeceras del WAF (específicas del proveedor) y un ID de petición que puedas rastrear.
- Correlaciona marca temporal e IP de cliente entre los logs del CDN/proxy y del origen.
Segundo: identifica la regla exacta y el componente exacto de la petición
- Extrae la entrada del evento/auditoría del WAF: ID/grupo de regla, variable coincidente (ARGS, REQUEST_URI, REQUEST_HEADERS), fragmento del payload que coincidió.
- Determina si coincidió en URI, parámetro de consulta, cuerpo, cookie o cabecera.
- Confirma si la regla está en modo block o en modo log/count.
Tercero: elige la palanca de ajuste menos peligrosa
- Corrige la app/solicitud si es legítimamente extraña (plugin malo, codificación rota, cookies enormes).
- Restringe el alcance usando URI + método + estado de autenticación (solo admin) antes de considerar deshabilitar una regla.
- Excluye la variable más pequeña (un solo parámetro) en lugar de todo un grupo de reglas.
- Aumenta los umbrales de anomalía solo cuando puedas demostrar que el tráfico es seguro y tengas controles compensatorios.
Hechos y contexto interesantes (porque la historia se repite)
- ModSecurity empezó a principios de los 2000 como un módulo de Apache—los WAFs eran originalmente un “parche caliente” para aplicaciones vulnerables, no una bala de plata.
- El OWASP Core Rule Set (CRS) se convirtió en el “cerebro” WAF genérico por defecto para muchas pilas; está diseñado para ser amplio, no específico de WordPress.
- El endpoint XML-RPC de WordPress fue importante a finales de los 2000 para publicación remota; hoy se abusa comúnmente para fuerza bruta y patrones de amplificación.
- Gutenberg (el editor por bloques) empujó a WordPress hacia un mayor uso de la REST API; muchos problemas “repentinos” con WAF aparecieron tras actualizaciones del editor porque la forma de las peticiones cambió.
- Las primeras implementaciones de WAF a menudo funcionaban en modo “solo detección” durante semanas; bloquear de inmediato causó interrupciones. Los equipos maduros todavía prueban cambios de WAF como si fueran código.
- Los modelos de “seguridad positiva” (permitir patrones conocidos buenos) son anteriores al marketing moderno de WAF; son más difíciles de operar, pero más robustos para endpoints administrativos.
- Algunas firmas de detección de SQLi tienen décadas; los atacantes se adaptan, pero también las aplicaciones—los falsos positivos frecuentemente provienen de cuerpos JSON modernos que activan patrones legados.
- Los CDN popularizaron las reglas gestionadas de WAF y hicieron común “WAF delante de todo”; la ventaja es la escala, la desventaja es que ahora depuras un motor de políticas distribuido.
Principios para ajustar el WAF de forma segura (sin teatro de seguridad)
1) No negocies con 403s; recopila evidencia
Cada producto WAF tiene una forma de decirte qué regla se disparó y qué coincidió. Si no puedes ver eso, estás operando a ciegas. Cuando los interesados demandan una solución instantánea, da una triage inmediata: “Puedo detener la hemorragia evitando el WAF para esta ruta, pero necesito 30 minutos para hacerlo de forma segura.” Luego hazlo de forma segura.
2) Las excepciones deben ser más estrechas que tu paciencia
Las buenas excepciones se acotan por:
- Ruta URI (
/wp-json/vs “todo WordPress”) - Método HTTP (permitir
GETpero escrutarPOST) - Contexto de autenticación (cookie de admin presente, o peticiones desde VPN, o desde un IdP conocido)
- Nombre de parámetro (excluir el campo
content, no todo el cuerpo) - ID de regla (deshabilitar una regla en una ubicación, no todo un conjunto gestionado)
3) Prefiere “arreglar el disparador” sobre “desactivar el detector”
Si un plugin envía un parámetro que parece SQLi porque incluye select y union como palabras planas, la solución correcta puede ser: codificar adecuadamente, cambiar nombres de campo, reducir la reflexión o sanitizar contenido antes. Ajustar el WAF no reemplaza la higiene de la aplicación.
4) Haz espacio para controles aburridos
WordPress se beneficia enormemente de mitigaciones aburridas y fiables: limitación de tasa en el inicio de sesión, filtrado de bots, MFA, cuentas administrativas con privilegios mínimos, bloqueo de endpoints no usados y mantener plugins actualizados. Ajustar el WAF es menos aterrador cuando tienes controles compensatorios.
Broma #1: Desactivar el WAF para arreglar WordPress es como quitar los detectores de humo porque no te gusta el pitido. Funciona hasta que deja de hacerlo.
Tareas prácticas: comandos, salidas y decisiones
A continuación hay tareas prácticas que puedes ejecutar en un origen Linux típico con Nginx/Apache y ModSecurity, además de algunas comprobaciones relacionadas con CDN/WAF. Cada tarea incluye: comando, qué significa la salida y qué decisión tomar.
Task 1: Reproducir el bloqueo con una petición conocida y capturar cabeceras
cr0x@server:~$ curl -kisS https://example.com/wp-login.php | sed -n '1,20p'
HTTP/2 403
date: Fri, 27 Dec 2025 10:14:22 GMT
content-type: text/html; charset=UTF-8
server: cloudflare
cf-ray: 86a1c2d3e4f56789-FRA
Qué significa: HTTP 403 con una cabecera de Cloudflare sugiere bloqueo en el edge WAF, no en tu aplicación de origen. El valor cf-ray es tu manejador de rastro.
Decisión: Extrae el evento del WAF usando el ID de petición (o timestamp+IP) antes de tocar reglas del origen.
Task 2: Comprobar la respuesta del origen directamente (evitar CDN) para probar dónde ocurre el bloqueo
cr0x@server:~$ curl -kisS --resolve example.com:443:203.0.113.10 https://example.com/wp-login.php | sed -n '1,20p'
HTTP/2 200
date: Fri, 27 Dec 2025 10:15:01 GMT
content-type: text/html; charset=UTF-8
server: nginx
Qué significa: El origen devuelve 200, el edge devuelve 403. El WAF/CDN está bloqueando, no WordPress.
Decisión: Ajusta las reglas del WAF en el edge. No pierdas tiempo en los logs de PHP todavía.
Task 3: Identificar qué endpoint está bloqueado (admin, REST, AJAX, subidas)
cr0x@server:~$ curl -kisS https://example.com/wp-json/wp/v2/users/me | sed -n '1,15p'
HTTP/2 403
date: Fri, 27 Dec 2025 10:16:11 GMT
content-type: application/json
server: cloudflare
Qué significa: La REST API está bloqueada. Eso puede romper Gutenberg, medios y las interfaces de plugins.
Decisión: Busca reglas gestionadas que apunten a cuerpos JSON, cabeceras de autenticación o firmas genéricas de “ataque a API”; ajusta por /wp-json/ y no globalmente.
Task 4: Si ModSecurity está en el origen, confirma que está habilitado y en qué modo
cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -E 'modsecurity|SecRuleEngine' -n
121:modsecurity on;
122:modsecurity_rules_file /etc/nginx/modsec/main.conf;
Qué significa: Nginx está compilado con el conector ModSecurity y está habilitado.
Decisión: Siguiente paso: comprueba si ModSecurity está en DetectionOnly o On e inspecciona los logs de auditoría.
Task 5: Comprobar la configuración SecRuleEngine
cr0x@server:~$ sudo grep -R --line-number 'SecRuleEngine' /etc/nginx/modsec
/etc/nginx/modsec/main.conf:12:SecRuleEngine On
Qué significa: Las peticiones pueden ser bloqueadas activamente en el origen.
Decisión: Si estás en medio de un incidente, considera cambiar temporalmente a DetectionOnly para una ubicación específica mientras analizas (no un interruptor general de apagado).
Task 6: Localizar el log de auditoría de ModSecurity y confirmar que recibe eventos
cr0x@server:~$ sudo grep -R --line-number 'SecAuditLog' /etc/nginx/modsec
/etc/nginx/modsec/main.conf:45:SecAuditLog /var/log/modsec_audit.log
cr0x@server:~$ sudo tail -n 3 /var/log/modsec_audit.log
--c9a1b7d8-H--
Message: Access denied with code 403 (phase 2). Matched "Operator `Rx' with parameter `(?i:(?:union(?:.*)select))'" against variable `ARGS:query' (Value: `union select`) [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf"] [id "942100"] [severity "CRITICAL"] [tag "application-multi"] [hostname "example.com"] [uri "/wp-admin/admin-ajax.php"] [unique_id "1735294592"]
Qué significa: La regla 942100 (categoría SQLi) se disparó en ARGS:query en admin-ajax.php. Eso es lo suficientemente específico como para afinar con precisión.
Decisión: No desactives todas las reglas SQLi. Crea una exclusión dirigida para el parámetro query en ese endpoint si validas que es seguro y esperado.
Task 7: Correlacionar la petición en los access logs (método, tamaño, user agent, IP cliente)
cr0x@server:~$ sudo awk '$7 ~ /admin-ajax\.php/ {print $1,$4,$6,$7,$9,$10,$12}' /var/log/nginx/access.log | tail -n 5
198.51.100.23 [27/Dec/2025:10:16:31 +0000] "POST /wp-admin/admin-ajax.php 403 721 "Mozilla/5.0"
198.51.100.23 [27/Dec/2025:10:16:34 +0000] "POST /wp-admin/admin-ajax.php 403 721 "Mozilla/5.0"
Qué significa: 403 repetidos en POST a admin-ajax. La IP cliente es estable; parece un navegador real.
Decisión: Si el mismo bloqueo afecta a muchos usuarios reales en distintas IPs, probablemente sea un falso positivo. Si es una IP que ataca repetidamente, puede ser un atacante—no afines, bloquea.
Task 8: Extraer valores de parámetros coincidentes del log de auditoría para ver qué dispara
cr0x@server:~$ sudo grep -n 'Matched "Operator' -n /var/log/modsec_audit.log | tail -n 2
4128:Message: Access denied with code 403 (phase 2). Matched "Operator `Rx' with parameter `(?i:(?:union(?:.*)select))'" against variable `ARGS:query' (Value: `union select`) [id "942100"] [uri "/wp-admin/admin-ajax.php"] [unique_id "1735294592"]
Qué significa: El valor literal union select está presente. Podría ser una carga maliciosa, o podría ser una sintaxis de búsqueda/filtro legítima que permite palabras clave SQL (mala idea, pero ocurre).
Decisión: Valida la función que lo genera. Si es una sintaxis de búsqueda legítima, necesitas controles compensatorios (solo autenticado, comprobaciones de nonce, limitación de tasa) antes de excluir.
Task 9: Confirmar el contexto de nonce/autenticación de WordPress para llamadas admin-ajax
cr0x@server:~$ curl -kisS -X POST https://example.com/wp-admin/admin-ajax.php \
-d 'action=test_action&_ajax_nonce=deadbeef&query=union%20select' | sed -n '1,25p'
HTTP/2 403
date: Fri, 27 Dec 2025 10:18:12 GMT
content-type: text/html; charset=UTF-8
Qué significa: Sigue bloqueado en el WAF; ni siquiera llegas a la validación de nonce de WordPress.
Decisión: Si planeas una excepción para admin-ajax, acótala a sesiones administrativas autenticadas (donde sea posible) o a acciones/parámetros específicos en lugar de un bypass general de admin-ajax.
Task 10: Para Apache + ModSecurity, confirma qué conjunto de reglas está cargado (CRS) y pistas de versión
cr0x@server:~$ sudo apachectl -M 2>/dev/null | grep -i security
security2_module (shared)
cr0x@server:~$ sudo grep -R --line-number 'owasp-crs' /etc/apache2 | head
/etc/apache2/mods-enabled/security2.conf:15:IncludeOptional /usr/share/modsecurity-crs/owasp-crs.load
Qué significa: Estás usando OWASP CRS. IDs de regla como 942100 se mapean a CRS SQLi.
Decisión: Usa mecanismos de exclusión soportados por CRS; evita editar archivos de reglas del proveedor directamente (hace que las actualizaciones sean un dolor).
Task 11: Crear una exclusión estrecha de ModSecurity (ejemplo) y validar sintaxis
cr0x@server:~$ sudo tee /etc/nginx/modsec/wordpress-exclusions.conf >/dev/null <<'EOF'
# Narrow exclusion: only for admin-ajax.php, only remove one parameter from one rule
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" "id:1001001,phase:1,pass,nolog,ctl:ruleRemoveTargetById=942100;ARGS:query"
EOF
cr0x@server:~$ sudo grep -R --line-number 'wordpress-exclusions' /etc/nginx/modsec/main.conf
72:Include /etc/nginx/modsec/wordpress-exclusions.conf
cr0x@server:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Qué significa: Eliminaste el parámetro query de la inspección por la regla 942100 solo en esa URI. Todo lo demás sigue siendo inspeccionado.
Decisión: Recarga y prueba. Si los ataques se desplazan a otros parámetros, verás nuevas coincidencias de reglas. Está bien; iteras con evidencia.
Task 12: Recargar y volver a probar la petición fallida exacta
cr0x@server:~$ sudo systemctl reload nginx
cr0x@server:~$ curl -kisS -X POST https://example.com/wp-admin/admin-ajax.php \
-d 'action=test_action&_ajax_nonce=deadbeef&query=union%20select' | sed -n '1,25p'
HTTP/2 200
date: Fri, 27 Dec 2025 10:19:44 GMT
content-type: text/html; charset=UTF-8
Qué significa: El WAF ya no bloquea esta petición. Ahora debes asegurarte de que WordPress rechace nonces inválidos y comportamientos no autenticados como corresponde.
Decisión: Confirma que la capa de aplicación sigue validando auth/nonce; si no lo hace, tu “arreglo” del WAF acaba de convertirse en una vía de explotación.
Task 13: Verificar que WordPress aún bloquea acciones peligrosas no autenticadas
cr0x@server:~$ curl -kisS -X POST https://example.com/wp-admin/admin-ajax.php \
-d 'action=delete_user&user_id=1' | sed -n '1,30p'
HTTP/2 200
date: Fri, 27 Dec 2025 10:20:12 GMT
content-type: text/html; charset=UTF-8
0
Qué significa: Muchos endpoints AJAX de WordPress devuelven 0 para no autorizados. Eso es razonablemente bueno; significa que llegas a WordPress y este deniega la acción.
Decisión: Si ves una respuesta exitosa para acciones privilegiadas sin autenticación, detente. Revierte la exclusión y arregla primero la validación de auth/nonce.
Task 14: Comprobar si cookies grandes activan límites de cabeceras (un disparador sigiloso del WAF)
cr0x@server:~$ curl -kisS https://example.com/ | awk 'tolower($0) ~ /^set-cookie:/ {print length($0), $0}' | head
148 Set-Cookie: wordpress_test_cookie=WP%20Cookie%20check; path=/
312 Set-Cookie: wp-settings-1=libraryContent%3Dbrowse; path=/; expires=Sat, 26 Dec 2026 10:20:55 GMT
Qué significa: No son enormes, pero si ves líneas de cookie muy largas (miles de bytes), algunos WAFs/proxies rechazarán peticiones luego cuando las cabeceras Cookie se hinchen.
Decisión: Si las cookies son masivas, reduce el bloat de cookies (limpia plugins, restringe alcance de cookies, evita almacenar estado en cookies) en lugar de ajustar el WAF para aceptar cabeceras absurdas.
Task 15: Detectar 429s (limitación de tasa) vs 403s (firma) en logs
cr0x@server:~$ sudo awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head
981 200
114 403
62 404
27 429
Qué significa: Algunas peticiones están limitadas por tasa (429). Esa es una vía de ajuste diferente a la de falsos positivos por firmas.
Decisión: Si los editores reciben 429 durante trabajo normal de administración, ajusta los límites de tasa por endpoint y método. No debilites reglas SQLi/XSS para resolver limitación de tasa.
Task 16: Confirmar preservación de la IP cliente real (el ajuste del WAF falla si las IPs están mal)
cr0x@server:~$ sudo grep -R --line-number 'real_ip_header|set_real_ip_from' /etc/nginx | head -n 20
/etc/nginx/conf.d/realip.conf:1:real_ip_header CF-Connecting-IP;
/etc/nginx/conf.d/realip.conf:2:set_real_ip_from 173.245.48.0/20;
/etc/nginx/conf.d/realip.conf:3:set_real_ip_from 103.21.244.0/22;
Qué significa: Nginx confía en una cabecera y en rangos de IP del proxy específicos. Sin esto, la correlación de límites de tasa y WAF puede ser un sinsentido.
Decisión: Si ves todas las IPs de clientes como la IP del proxy, corrige el manejo de la IP real antes de ajustar reglas de comportamiento.
Patrones de ajuste que funcionan (y su coste)
Patrón A: Excluir un parámetro de una regla en un endpoint
Este es el estándar de oro para falsos positivos. Mantienes la regla activa en todas partes. Mantienes otras reglas activas en ese endpoint. Además preservas el registro de auditoría.
Mejor para: campos de contenido de Gutenberg/editor, campos de búsqueda, claves de payload JSON que parecen código.
Coste: Debes mantener un pequeño mapa de “parámetros ruidosos conocidos”. No es un coste real; se llama responsabilizarse del sistema.
Patrón B: Políticas separadas para tráfico administrativo vs público
Los endpoints administrativos se comportan distinto. También deberían protegerse distinto. En muchas organizaciones lo mejor es poner la administración detrás de controles adicionales:
- Requerir MFA / SSO
- Restringir por VPN o IPs corporativas de salida (si es factible)
- Límites de tasa más estrictos en login; más permisivos en cuerpos POST del editor donde existe HTML legítimo
Mejor para: sitios con muchos editores y uso administrativo intenso.
Coste: Más objetos de política y pruebas. Aún más barato que tiempos de inactividad y whitelists por pánico.
Patrón C: Mover reglas gestionadas de “block” a “count” temporalmente—de forma quirúrgica
A veces no sabes si es un falso positivo hasta que recoges más eventos. Poner un grupo de reglas gestionadas en modo count/detect-only para un endpoint durante una ventana corta puede ayudar.
Mejor para: incidentes con alto impacto de negocio y evidencia de coincidencia poco clara.
Coste: Riesgo a corto plazo. Debes compensar con limitación de tasa y monitorización durante la ventana.
Patrón D: Bloquear lo que no usas (esto reduce tanto el riesgo como el ruido del WAF)
Si no necesitas /xmlrpc.php, bloquéalo en el edge o en el origen. Lo mismo para rutas REST no usadas o endpoints heredados. Menos superficie significa menos payloads raros y menos colisiones con reglas.
Broma #2: XML-RPC es como esa impresora vieja en la esquina—nadie admite que la usa, pero sigue causando incidentes.
Tres microhistorias corporativas de sistemas reales
Microhistoria 1: El incidente causado por una suposición errónea
Una empresa mediana migró su sitio de marketing a WordPress detrás de un WAF gestionado. La checklist de la migración decía “activar reglas gestionadas, poner en modo block, lanzar”. La suposición fue que las reglas gestionadas son “seguras por defecto”. Normalmente lo son. Hasta que no lo son.
El primer lunes, los editores empezaron a usar un nuevo plugin de constructor de páginas que guardaba diseños como grandes blobs JSON vía /wp-json/. El WAF comenzó a bloquear peticiones como “ataque de protocolo: JSON inválido” y “XSS en cuerpo” porque el constructor almacenaba fragmentos HTML y estilos inline dentro de cadenas JSON.
Ingeniería asumió que era un bug de WordPress. Marketing asumió que ingeniería “bloqueaba la creatividad”. Seguridad asumió que marketing subía malware. Todos estaban equivocados a su manera.
La solución no fue desactivar el WAF. Sacaron los logs de eventos del WAF, encontraron dos reglas ruidosas y excluyeron claves JSON específicas en la ruta /wp-json/ mientras mantenían activas las reglas XSS en otros lugares. También añadieron un límite de tamaño y normalizaron la codificación en el borde de la aplicación. El incidente terminó, y la discusión terminó tiempo después.
Microhistoria 2: La optimización que salió mal
Una organización grande decidió “optimizar rendimiento” cacheando más agresivamente en el CDN. Añadieron una regla: cachear todo bajo /wp-json/ durante 10 minutos. También mantuvieron el WAF estricto en APIs porque “las APIs son peligrosas.” Dos decisiones, razonables por separado, combinaron en caos.
Los editores vieron fallos aleatorios de autorización e 403 intermitentes. Algunas respuestas 200 cachéadas (desde un contexto autenticado) se sirvieron a clientes no autenticados, lo que activó comprobaciones de bots y desajustes de política. Mientras tanto, los nonces de WordPress expiraban de forma impredecible porque la UI mezclaba respuestas cachéadas y en vivo.
Ingeniería primero ajustó el WAF, porque 403 significaba “problema WAF”. Ese ajuste lo empeoró: ahora el contenido cachéado incorrecto fluía con más libertad. La causa raíz real fue cachear un endpoint que nunca debió cachearse sin reglas de vary y segmentación de autenticación cuidadosas.
Revirtieron el cacheado para rutas REST autenticadas, mantuvieron cacheo solo para rutas públicas GET explícitas y endurecieron reglas WAF alrededor de login y XML-RPC. El rendimiento mejoró, las tasas de error bajaron y todos aprendieron la misma lección que se repite: “más rápido” no es una característica si está mal aplicada.
Microhistoria 3: La práctica aburrida pero correcta que salvó el día
Una empresa regulada ejecutaba WordPress para un portal de clientes. Su equipo de seguridad requería que todo cambio de WAF pasara por una pequeña pipeline: modo solo registro por 24 horas, revisar coincidencias principales, luego aplicar. Sonaba burocrático. También fue la razón por la que dormían tranquilos.
Un día, una actualización de plugin cambió un formulario de administración para enviar contenido con un parámetro llamado template que contenía trozos de HTML y shortcodes. El WAF empezó a detectarlo como “inyección PHP” en páginas admin. Como el proceso de cambio del WAF incluía dashboards base y un modo de staging, notaron el pico antes de poner la regla actualizada en bloqueo en producción.
Implementaron una exclusión limitada a URIs administrativas autenticadas y al parámetro específico, luego añadieron una prueba de unidad que publica el payload exacto a través del WAF en CI. Aburrido. Previsible. Efectivo.
Cuando un auditor preguntó “¿cómo evitan la desactivación de controles de seguridad en emergencia?”, tuvieron una respuesta que no fue una promesa. Fue evidencia.
Errores comunes: síntoma → causa raíz → solución
1) Los editores no pueden guardar entradas; Gutenberg muestra “Updating failed”
Síntoma: 403/406 intermitente en peticiones POST a /wp-json/.
Causa raíz: una regla WAF coincide en el cuerpo JSON (fragmentos HTML, scripts inline o CSS) o reglas de parser JSON estrictas.
Solución: Excluir claves/parametros JSON específicos para IDs de regla concretos en POST de /wp-json/, mantener el logging. Confirma que la autenticación y validación de nonce de WordPress sigue aplicada.
2) Las subidas de medios fallan con 403; solo fallan archivos grandes
Síntoma: archivos pequeños suben, los grandes fallan; a veces aparece 413 también.
Causa raíz: límites de tamaño del cuerpo en WAF/CDN/origen; reglas de parseo multipart; timeouts de integración antivirus.
Solución: Alinea límites entre capas (CDN, WAF, Nginx/Apache, PHP). Prefiere aumentar límites en lugar de evitar inspección para subidas; si debes evitarla, añade comprobaciones estrictas de tipo de archivo y escaneo de malware.
3) wp-admin funciona desde VPN pero no desde redes domésticas
Síntoma: 403 solo para algunos ISP/regiones; la administración parece “bot”.
Causa raíz: protección de bots o reglas de reputación demasiado agresivas; faltan cookies/desafíos por configuraciones de privacidad; o tu origen interpreta mal la IP de cliente y activa límites de tasa.
Solución: Corrige el manejo de IP real; ajusta reglas de bots específicamente para /wp-login.php y /wp-admin/; considera desafíos de paso solo en el login.
4) GETs de la REST API son bloqueados pero las páginas normales funcionan
Síntoma: el sitio público carga, pero integraciones fallan.
Causa raíz: el grupo de reglas gestionadas “protección API” bloquea patrones comunes en parámetros de consulta REST.
Solución: Permitir rutas REST y métodos específicos; excluir solo los nombres de parámetros ofensivos de IDs de regla concretos; añadir limitación de tasa y comprobaciones de autenticación para endpoints sensibles.
5) Todo bajo /wp-admin/ se pone en lista blanca “temporalmente” y luego nunca se revierte
Síntoma: el equipo de seguridad se queja después; encuentras una regla de bypass con meses de antigüedad.
Causa raíz: solución de incidente sin plan de rollback; no hay expiración en políticas de emergencia.
Solución: Añade expiración explícita para excepciones de emergencia, además de alertas para “reglas de bypass presentes”. Incorpóralo en gestión de cambios, no en la memoria humana.
6) El WAF bloquea HTML legítimo en el contenido de posts
Síntoma: guardar una entrada activa reglas XSS.
Causa raíz: el WAF ve HTML y piensa “ataque”. El contenido de WordPress es literalmente HTML. Sorprendente, lo sé.
Solución: Excluir campos de contenido solo para endpoints del editor; asegúrate de que la sanitización KSES de WordPress esté habilitada y que no estés otorgando unfiltered_html a todos.
7) La fuerza bruta de login sigue teniendo éxito tras ajustar
Síntoma: el ajuste redujo bloqueos pero aumentó el éxito de ataques.
Causa raíz: debilitaste protecciones relacionadas con autenticación (limitación de tasa, comprobaciones de bots) mientras perseguías falsos positivos en otros lugares.
Solución: Separa políticas: estrictas en /wp-login.php, /xmlrpc.php y admin; excepciones medidas en payloads de editor/REST.
Listas de verificación / plan paso a paso
Plan paso a paso para ajustar sin crear un agujero
- Captura una petición fallida: timestamp, IP cliente, URI, método, ID de petición (header de rastro en el edge si existe).
- Prueba la capa que bloquea: edge vs origen usando
curl --resolveo acceso directo al origen desde una red de confianza. - Extrae el evento del WAF: ID/grupo de regla, variable coincidente, fragmento del payload, acción tomada (block/challenge/log).
- Clasifica el evento:
- ¿Parece un ataque desde IPs aleatorias? Trátalo como positivo verdadero.
- ¿Ocurre con editores autenticados en muchas IPs? Probablemente falso positivo.
- ¿Mayormente 429? Es limitación de tasa, no firmas.
- Elige la palanca más pequeña:
- Excluir un parámetro objetivo de una regla ID en una URI, o
- Acotar la excepción por método + URI + contexto autenticado, o
- Poner temporalmente en count/detect-only un grupo para un endpoint mientras reúnes evidencia.
- Añade controles compensatorios cuando aflojes la inspección:
- Asegura que WordPress aplica checks de nonce/autenticación
- Limitación de tasa en login y admin-ajax por IP/usuario
- Bloquea endpoints no usados como XML-RPC si no son necesarios
- Prueba la falla exacta y algunos casos de abuso: repite la petición que fallaba; también intenta payloads obvios y asegúrate de que sigan siendo bloqueados en algún lugar adecuado.
- Monitorea deriva: nuevas coincidencias top de WAF, aumento de 5xx, más fallos de autenticación o caídas súbitas en bloqueos (señal de que sobre-bypassaste).
- Documenta la excepción: endpoint, ID de regla, motivo, responsable, fecha de expiración y plan de reversión.
Lista operativa para disponibilidad en producción
- Los logs del WAF son accesibles, buscables y retenidos el tiempo suficiente para depurar incidentes.
- Los endpoints administrativos tienen controles más fuertes que los públicos (separación de políticas).
- Las reglas de bypass de emergencia tienen expiración y alertas.
- Existe limitación de tasa para
/wp-login.phpy/xmlrpc.php. - La IP real del cliente se propaga correctamente a logs de origen y herramientas de seguridad.
- Staging puede reproducir el comportamiento del WAF (o tienes una ventana segura “count-only” para recoger evidencia).
FAQ
1) ¿Debería simplemente poner en lista blanca /wp-admin/ en el WAF?
No. Ahí residen las acciones más valiosas. Si debes reducir fricción, acota excepciones a endpoints y parámetros administrativos específicos y mantén las protecciones de inicio de sesión estrictas.
2) ¿Es seguro desactivar un ID de regla OWASP CRS?
A veces, pero hazlo como una cirugía: desactiva o elimina el objetivo más pequeño posible, y solo para el endpoint donde se ha demostrado que es ruidoso. No uses “ruleRemoveById” globalmente a menos que disfrutes explicaciones tras incidentes.
3) ¿Por qué admin-ajax.php dispara tantas reglas WAF?
Porque es un endpoint POST de alto tráfico con muchos parámetros y acciones definidas por plugins. A los atacantes les gusta, los plugins lo abusan, y el WAF nota ambos.
4) ¿Debería bloquear xmlrpc.php?
Si no lo necesitas explícitamente, sí—bloquéalo en el edge y en el origen. Si lo necesitas, limita fuertemente la tasa y considera permitir solo clientes conocidos.
5) ¿Cómo sé si es un falso positivo o un ataque real?
Mira el contexto: ¿es tráfico de editores autenticados, user agents consistentes y reproducible desde flujos normales? ¿O es pulverización de IPs aleatorias con payloads obvios? Usa logs de auditoría: la variable coincidente + fragmento del payload suele dejarlo claro.
6) ¿Puede el cache causar bloqueos del WAF?
Indirectamente, sí. Un cache mal hecho de respuestas REST autenticadas puede crear desajustes raros y reintentos repetidos que activan reglas de bots/limitación. Arregla la estrategia de cache antes de aflojar reglas de seguridad.
7) ¿Cómo ajusto reglas de WAF sin perder visibilidad?
Mantén el logging activo incluso cuando excluyes objetivos. Usa modos count/detect-only para ventanas cortas de recopilación de evidencia. Construye dashboards para los IDs de regla principales y URIs afectadas para que las excepciones no queden invisibles para siempre.
8) ¿Cuál es la forma más segura de manejar HTML del editor que dispara reglas XSS?
Excluye el campo de contenido específico en el endpoint del editor para reglas XSS concretas, y asegúrate de que la sanitización de WordPress y las capacidades de usuario estén configuradas correctamente.
9) ¿Qué hago si el proveedor del WAF no me muestra el payload que coincidió?
Entonces necesitas mejores registros en alguna capa que controles (logs de auditoría del WAF en origen, logs de depuración del proxy reverso con precaución, o un entorno de staging). Afinar sin datos de coincidencia es adivinar con papeleo.
Siguientes pasos prácticos
Haz tres cosas hoy, en este orden:
- Consigue trazabilidad: asegura que cada bloqueo te dé un ID de petición y un ID de regla que puedas buscar. Si eso no existe, arregla el logging antes de que el próximo incidente te obligue a “evadir temporalmente” y te quedes sin salida.
- Separa políticas por endpoint: trata
/wp-login.php,/xmlrpc.php,/wp-admin/,/wp-json/y/admin-ajax.phpcomo productos distintos con perfiles de riesgo distintos. - Implementa exclusiones estrechas con expiración: excluye parámetros de reglas específicas, no grupos enteros. Añade un responsable y una fecha de expiración, y convierte la reversión en algo rutinario.
Tu WAF debería ser un bisturí, no una venda en los ojos. Si está bloqueando WordPress, no necesitas menos seguridad—necesitas un ajuste mejor.