Resumen: mantengo un WAF con ModSecurity y el ruleset de OWASP delante de un buen número de webs WordPress. Con ese volumen, los falsos positivos son un goteo constante: un plugin que envía demasiado HTML, una cookie de analítica de terceros, un texto en inglés con la palabra equivocada. Este proyecto no es sobre bloquear ataques — es sobre el método que uso para resolver esos bloqueos legítimos en minutos en vez de en horas, sin abrir agujeros por el camino, con casos reales (anonimizados) que lo ilustran.
El contexto: un WAF compartido, muchos sitios distintos
Las webs de mis clientes van detrás de Cloudflare y de un proxy de entrada con ModSecurity, que aplica el mismo conjunto de reglas a todas. Es una defensa sólida contra inyección SQL, XSS, ejecución remota de comandos y demás familias clásicas. El problema es que WordPress, sus page builders y sus plugins de terceros no siempre juegan bien con un motor que analiza cada parámetro con reglas genéricas: guardan HTML completo en un campo, mandan cookies con JSON dentro, o usan palabras normales que un analizador de patrones interpreta como un comando de sistema. Cuando eso pasa, un cliente o un proveedor externo reporta que «la web da error al guardar» y a mí me toca averiguar por qué sin desproteger nada.
El método: tres pasos y ya
Durante un tiempo intenté ser más metódico de lo necesario: reproducir el bloqueo con payloads sintéticos, extraer fragmentos del log de auditoría, proponer exclusiones para rutas que nadie había pedido. Aprendí por las malas que eso sobra casi siempre: el dato de qué pasó ya está en los logs, no hace falta reconstruirlo desde cero. El método que uso ahora es corto:
- Qué IP provocó el bloqueo (se descarta cualquier prueba hecha por mí mismo desde mi propia conexión).
- Qué reglas exactas saltaron para esa IP, cruzando el identificador del bloqueo con el log de auditoría del WAF.
- Parche mínimo de esas reglas concretas, acotado siempre a un host y una ruta — nunca una exclusión global ni un plugin desactivado por completo.
El orden importa: sin el paso 2, es fácil dar por buena la familia de reglas que dice el proveedor del plugin, y casi siempre se equivoca (más abajo hay un ejemplo). Y sin acotar por host y ruta en el paso 3, la exclusión se cuela para todas las webs del clúster, no solo la que la necesitaba.

Un gotcha que hace perder tiempo si no lo sabes
La página de aviso que ve el visitante cuando el WAF bloquea algo se sirve también para los errores normales del propio WordPress — por ejemplo, un formulario que rechaza un token de seguridad caducado durante una prueba manual. Ver esa página no demuestra que haya sido el WAF. La única prueba válida es mirar el log de auditoría: si no hay una transacción registrada para esa petición, el bloqueo no vino de ahí. El control barato es mandar un payload inocuo y comparar el tamaño de la respuesta: si es idéntico al del «ataque» reportado, el problema está en el backend, no en el WAF.
Por qué el inglés normal dispara reglas de ejecución de comandos
Las familias de reglas que detectan inyección de comandos Unix comparan fragmentos de texto contra una lista de comandos habituales del sistema (ls, cat, who, time, sort, cut, top…). El problema es que esas mismas palabras aparecen todo el rato en prosa inglesa corriente. Tuve un caso de manual: la web de una agencia de viajes cliente guardaba las preguntas frecuentes de una excursión, y una de las respuestas decía algo como «if you manage your time poorly, you may arrive late». La palabra time, sola, en mitad de una frase perfectamente normal, bastó para que el guardado se bloqueara como un intento de ejecución de comandos.
Lo curioso de estos casos es que no son reproducibles con payloads raros: no hay manera de «atacar» ese endpoint de forma más agresiva y ver algo distinto, porque el disparador es contenido legítimo del propio cliente. Cualquier texto en inglés que mencione horarios, ordenar una lista o «recortar» algo puede caer en la misma trampa. La exclusión, aquí, no puede acotarse a un parámetro concreto porque el campo cambia de posición según cuántas preguntas tenga el formulario ese día — así que se acota por ruta del endpoint, y solo a las reglas de esa familia, dejando el resto del motor intacto sobre el mismo parámetro.
Casos reales (anonimizados)
Un plugin de SEO que manda el artículo entero para puntuarlo
Un analizador SEO muy extendido envía el contenido completo del post a un endpoint propio cada vez que un editor escribe, para calcular su puntuación. En sitios con maquetadores visuales, ese contenido es HTML y shortcodes densos, y dispara de golpe varias familias de reglas (inyección de comandos, XSS, SQLi) solo por la cantidad de marcado que lleva dentro. Ya existía una exclusión genérica para los editores del núcleo de WordPress, pero su expresión no cubría el endpoint específico de este plugin, así que se quedaba fuera y seguía bloqueando cada guardado. El parche fue extender la exclusión por familia de reglas (no por identificador suelto, para no tener que repetirlo cada vez que otra regla de la misma familia decida saltar) y acotarlo a ese único parámetro del cuerpo de la petición. Contenido de maquetador legítimo pasa limpio; el mismo contenido puesto en otro campo del mismo endpoint sigue bloqueado, y un intento real de recorrido de directorios en el mismo parámetro también.
Una cookie de analítica de terceros tumbando el panel entero
Un cliente reportó un error puntual en una pantalla concreta del administrador de WordPress y todo apuntaba a un fallo del plugin de tienda que estaba usando en ese momento. Mirando el patrón, la pista real era otra: si absolutamente todas las URLs de ese sitio devuelven el mismo bloqueo con el mismo motivo, no es el endpoint que reporta el cliente — es una cookie o una cabecera que viaja en cada petición. Efectivamente: un plugin de maquetación visual traía de fábrica una integración de analítica de terceros, con una clave de proyecto fija en su propio código, que plantaba una cookie con estructura de documento (JSON) al abrir su editor. Esa estructura hacía saltar una regla anti-inyección pensada para bases de datos documentales, y desde ese momento el navegador del administrador arrastraba un bloqueo en cada petición durante meses, hasta que la cookie caducaba. La exclusión se acotó a esa cookie concreta, por nombre, y solo para esa familia de reglas — el resto de protecciones (XSS, ejecución remota) se quedan intactas incluso sobre el valor de esa misma cookie.
El admin sin estilos por un simple corte de texto
Mi caso favorito del catálogo: una pantalla del panel de administración de un cliente empezó a cargar sin ningún estilo visual, como HTML en crudo. No era caché, ni un plugin roto. WordPress construye internamente una lista larga de nombres de hojas de estilo y la corta en trozos para servirla; en esa pantalla en concreto, la lista era tan larga que el corte caía justo en mitad de un nombre de paquete, y el trozo resultante empezaba por las dos letras que también forman el comando Unix de listar ficheros. Una coincidencia de texto, cero intención maliciosa, bloqueado igualmente porque el motor no sabe que ese fragmento es parte de un nombre más largo. Se resolvió eximiendo esa familia de reglas para los dos endpoints internos que sirven esas listas — no hay superficie real que proteger ahí, solo texto concatenado por nombre de fichero.
Lo que aprendí
El patrón se repite: cuanto más intenta un plugin «ser eficiente» metiendo datos grandes en un solo campo, más fácil es que choque con una regla genérica. Y cuanto más confío en el diagnóstico del ticket del proveedor, más tiempo pierdo: en más de un caso el proveedor estaba seguro de que el bloqueo era por XSS cuando el audit decía, sin ambigüedad, que era otra familia completamente distinta. El dato manda, no la intuición de quien reporta.
La disciplina que más me ha ahorrado tiempo no es técnica, es de alcance: exclusión siempre por host y ruta, nunca global; por familia de reglas cuando el parámetro cambia de sitio, por regla suelta cuando no; y verificación siempre con una prueba de que lo malo real (una ruta transversal, una inyección de verdad) sigue bloqueado en el mismo sitio donde acabo de abrir la mano. No cuajó intentar prevenir el siguiente falso positivo adivinando qué plugin lo va a causar; sí ha sido sabroso limitarme a reaccionar rápido con el método de tres pasos cada vez que aparece uno nuevo.
Stack
- nginx + ModSecurity / OWASP CRS
- Ficheros de exclusión propios, ordenados por fase de carga (antes y después del ruleset)
- Cloudflare (capa de borde)
- WordPress (multisitio, varios page builders y plugins de terceros)
curlpara verificación dirigida al origen
Relacionado: Hardening my own server, sobre el resto de capas de seguridad delante de estas mismas webs.


