Hay un plugin con más de 30.000 instalaciones activas que lleva meses sin actualizar y arrastra una vulnerabilidad XSS reportada en WPScan que nadie ha corregido.
Me refiero a Twenty20 Image Before-After, y cualquier autor o colaborador con permisos para escribir puede dejarte un JavaScript malicioso guardado en una entrada esperando a que la abra el primer visitante, así que si lo tienes activo ahora mismo eres vulnerable.
No es un caso raro ni aislado, porque los ataques XSS (Cross-Site Scripting) están entre las vulnerabilidades más reportadas en aplicaciones web según el OWASP Top 10.
WordPress, por su naturaleza de CMS abierto con miles de plugins y temas de terceros que nadie audita en profundidad, acaba siendo objetivo recurrente, y Patchstack publica cientos de vulnerabilidades XSS al año solo en plugins WordPress, y muchas afectan a webs con decenas de miles de instalaciones activas que siguen sin parchear.
La buena noticia es que defenderse de XSS no requiere ser pentester ni gastar dinero en plugins premium, basta con entender qué es un XSS, cómo aterriza en una web WordPress y dónde te conviene frenarlo.
En esta guía te lo cuento aplicando la misma estrategia por capas que en la guía de defensa contra ataques de fuerza bruta, porque al final cualquier defensa que se precie se monta así, de fuera hacia dentro.
Pero primero…
Qué es un ataque XSS y por qué WordPress es objetivo recurrente
Un XSS es un ataque que consigue meter código JavaScript donde no debería, y que el navegador del visitante ejecuta como si fuera parte legítima de tu web.
Lo curioso del asunto es que el atacante no toca tu servidor, no roba contraseñas de admin ni tiene que saltar tu cortafuegos, le basta con que tu web pinte en pantalla algo que un usuario haya escrito sin filtrarlo bien. El daño potencial es enorme y va por dentro, no por fuera.
Un XSS bien colocado puede robar la cookie de sesión de un administrador para entrar en su nombre, redirigir a tus visitantes a una web falsa que parece la tuya, recolectar datos de formularios antes de que se envíen, convertir el navegador del visitante en parte de una botnet o desfigurar la web con propaganda.
Lo peor es que todo eso pasa en el navegador del visitante, así que tu servidor sigue funcionando como si nada y muchas veces ni te enteras de que estás comprometido.
WordPress es objetivo principal por dos razones que se refuerzan entre sí.
- La cuota de mercado, que ronda el 42% de las webs del planeta, así que si vas a atacar webs en serie atacas WordPress y multiplicas resultados.
- El modelo de plugins y temas, donde cualquiera puede subir código al repositorio o vender un tema en un marketplace y meterlo en miles de webs sin que nadie haga auditoría a fondo.
El núcleo de WordPress está bastante controlado, los plugins y temas son el coladero habitual y sino echa un vistazo a los resúmenes de actualidad WordPress que hago cada lunes para comprobarlo.
Los tres tipos de XSS, con ejemplos aplicados a WordPress
No todos los XSS funcionan igual, hay tres tipos clásicos y cada uno se cuela por una rendija distinta de tu WordPress. Saber distinguirlos te ayuda a saber dónde mirar cuando algo huele mal.
XSS reflejado
Es el más sencillo y el menos peligroso de los tres, aunque tampoco te confíes porque sigue haciendo daño. El código malicioso viaja en la URL y se ejecuta en cuanto el visitante hace clic en un enlace manipulado.
El escenario típico en WordPress es un plugin que muestra el parámetro de búsqueda en pantalla sin escaparlo, así que si el plugin recibe ?s=zapatillas y pinta «No hay resultados para zapatillas», un atacante puede preparar un enlace tipo tudominio.com/?s=<script>alert(1)</script> y mandárselo a la víctima por email, redes o un comentario en un foro.
Cuando la víctima hace clic, el navegador interpreta el <script> como código de tu web y lo ejecuta, y si en vez de un alert inofensivo lleva código que roba la cookie de sesión, el atacante puede entrar como ese usuario.
Otro ataque típico es el parámetro de mensaje de error o de notificación, esos ?msg=usuario+creado, ?error=... o ?notice=... que muchos plugins pintan tal cual, y donde cabe lo que quieras meter si no hay algún tipo de filtrado.
XSS almacenado
Este es el malo de la película, porque el código malicioso se queda guardado en tu base de datos y se ejecuta cada vez que alguien visita la página afectada.
Aquí no hace falta engañar a nadie con un enlace, basta con que la víctima entre a leer un artículo, un comentario o una ficha de producto y ya tiene el regalo servido.
El caso del plugin Twenty20 que abre este artículo es exactamente este, porque el plugin tenía un shortcode [twenty20] con varios atributos (img1, img2, before, after, offset, orientation) que no se filtraban antes de pintarse en pantalla, así que cualquier usuario con permisos para escribir contenido podía dejar guardado en una entrada algo así:
[twenty20 img1="javascript:alert(1)" img2="..." before="<script>alert(1)</script>"]
A partir de ese momento, cada visitante que abriese la entrada ejecutaba ese JavaScript, y si en vez de un alert hubiese un script que recopila cookies de admin o redirige al banco falso, el daño se multiplica por cada visita que reciba la entrada.
El XSS almacenado se cuela por sitios donde WordPress acepta que los usuarios introduzcan información, y la guarda para enseñarla luego.
Los candidatos típicos son:
- Comentarios mal filtrados.
- Descripciones de productos en WooCommerce con HTML permitido sin filtrar.
- Formularios de contacto que muestran el último envío en el panel del admin.
- Campos personalizados que una plantilla custom pinta en el frontend sin pasar por
esc_html() - Perfiles de autor con biografía editable.
- Ociones de plugins que se muestran en el escritorio sin escapar.
Donde algo entra y luego sale sin filtrar hay candidato a XSS almacenado.
XSS basado en DOM
El más moderno de los tres y el que peor cubren los WAF tradicionales, porque el ataque ocurre íntegramente en el navegador y tu servidor ni se entera de que ha pasado.
El JavaScript de tu propia web lee algo del URL o del almacenamiento local del navegador y lo pinta en pantalla, y si no filtra bien lo que ha leído, el atacante consigue meter su código.
En WordPress aparece sobre todo en carruseles, maquetadores, bloques personalizados y plugins de calculadoras o formularios, cualquier cosa que use mucho JavaScript en en la parte pública de la web.
Un caso típico es el carrusel que lee el hash de la URL (#slide=3) y pinta el contenido correspondiente, o el plugin de búsqueda con autocompletado que muestra el término que el usuario está tecleando en tiempo real.
El XSS DOM es engañoso porque no aparece en los logs del servidor, no lo detectan los escáneres clásicos y muchos plugins de seguridad lo dejan pasar al no encontrar nada raro en la petición HTTP, ya que lo malicioso está íntegramente en el JavaScript del cliente.
Errores fáciles de detectar aunque no seas técnico
No hace falta saber programar para descubrir si tu WordPress tiene problemas evidentes, hay tres pruebas tontas que puede hacer cualquiera en cinco minutos y que te dan pistas de sobra para saber si tienes que ponerte serio con la defensa.
- HTML básico: Consiste en escribir
<b>probando</b>en el formulario de comentarios o en cualquier campo de texto que se vaya a mostrar después, y enviarlo. Si al ver el resultado aparece la palabra «probando» en negrita, tienes un problema, porque tu web está aceptando HTML donde no debería, y aunque eso no es XSS en sí, sí es por donde se pueden colar. Si acepta<b>es muy probable que acepte<script>. - La prueba del parámetro reflejado: Añade a la URL de tu web un parámetro inventado con un valor distintivo, tipo
?test=hola12345, recorre la página y mira el código fuente con botón derecho buscando «hola12345». Si aparece tal cual, sin escapar, tienes un XSS reflejado en potencia porque algún plugin o el tema está pintando ese parámetro en algún sitio sin filtrarlo. - La prueba del campo personalizado: Esta consiste en editar cualquier campo que tu tema o plugin pinte en el frontend (una biografía de autor, un campo personalizado, un campo de perfil), meterle
<b>negrita</b>, guardar y recargar. Si sale en negrita en la parte pública de la web es que se pueden colar más cosas.
Estas pruebas no son pentesting, son sentido común aplicado en segundos, y si alguna falla sabes que tienes que ponerte las pilas con las capas que vienen a continuación.
Cómo saber si tu WordPress es vulnerable a ataques XSS
Las pruebas anteriores te dicen si hay agujeros evidentes, pero no te dan la foto completa, para eso necesitas herramientas que escaneen de verdad. Hay 4 gratuitas que van bien y son las que uso yo.
La primera es el analizador de seguridad que tienes en las herramientas WordPress, que hace más de 30 comprobaciones sin tener siquiera que entrar a tu web y revisa cabeceras de seguridad, exposición de información, scripts sospechosos y archivos sensibles. No te dice si tienes XSS concretos, pero sí si tu web está bien armada contra esa clase de ataques.
Si tienes acceso a la web, o simplemente es la tuya, una versión mejor de la anterior es el analizador incluido en el plugin gratuito Vigilante, que ya sí analiza además las tripas de tu WordPress, desde dentro con lo que el escaneo es más profundo, y efectivo.
Las otras dos son las bases públicas de vulnerabilidades de plugins y temas WPScan y Patchstack. Busca por nombre cada uno de tus plugins activos y mira si tienen XSS reportados sin parchear, y si alguno está como «closed for download» o lleva meses sin que el autor responda en los foros, plantéate sustituirlo, porque las vulnerabilidades no se arreglan solas.
Si quieres una pista rápida sin instalar nada, abre tu web en el navegador, pulsa F12 y mira la consola, porque cuando aparecen mensajes con palabras tipo «Refused to execute», «Content Security Policy violation» o referencias a scripts de dominios que no reconoces, algo está pasando que merece la pena investigar.
Dónde frenes el XSS lo cambia todo
Igual que pasa con la fuerza bruta, no es lo mismo frenar un XSS a tres calles de tu casa que frenarlo cuando ya ha llegado al portal, y con XSS hay una diferencia que pesa todavía más, que es el daño potencial.
Un XSS que llega a ejecutarse en el navegador de un visitante puede robar su cookie de sesión y mandársela al atacante en milisegundos, y si ese visitante era un administrador, el atacante tiene admin de tu WordPress sin haber pasado nunca por wp-login.
Una vez dentro te instala una puerta trasera, modifica plugins, mete malware en archivos del tema y vienen los disgustos de verdad, así que cuanto más lejos del navegador del visitante frenes el ataque, mejor para todos.
La defensa funciona como tres filtros encadenados, cada uno más fino que el anterior. Lo que se le escapa al primero lo retiene el segundo, lo que pasa el segundo lo pilla el tercero, y si los tienes los tres bien puestos llega muy poca cosa al fondo.
- La CDN es el filtro más grueso, el que frena los patrones XSS conocidos antes de que la petición toque tu servidor, y desde donde puedes inyectar cabeceras de seguridad que mitigan el daño aunque el XSS exista.
- El hosting es el filtro intermedio, el que bloquea peticiones maliciosas que han pasado la CDN, mantiene PHP actualizado para tapar agujeros conocidos a nivel servidor y aísla tu cuenta del resto.
- WordPress es el filtro fino, donde se mantiene todo el ecosistema actualizado, se gestionan cabeceras y CSP, se filtran entradas y se escapa la salida cuando tocas código.
Ninguno de los tres sustituye a los otros, así que si uno falla los demás siguen filtrando. Vamos a por cada uno.
Filtro 1: la CDN como primer barrido
Una CDN seria delante de tu WordPress es la mejor inversión gratuita que puedes hacer en seguridad.
En los ejemplos uso, como siempre, Cloudflare porque es el que utilizo y tiene plan gratuito de sobra, pero Bunny Shield, KeyCDN o Sucuri Firewall ofrecen funciones equivalentes y los conceptos que vienen valen para cualquiera.
WAF gestionado contra patrones XSS
- Qué consigues: Que las peticiones con payloads XSS conocidos no lleguen ni siquiera a tu servidor, porque el filtrado ocurre en el borde de la red de Cloudflare a miles de kilómetros de tu hosting.
- Cómo funciona: El WAF aplica reglas basadas en el OWASP Core Rule Set que detectan patrones típicos de XSS como
<script>en parámetros de URL,javascript:en valores donde se espera un URL,onerror=y compañía, y las peticiones que coinciden con esos patrones se bloquean o reciben un desafío automático. - Cómo se hace en Cloudflare: El plan gratuito ya trae el OWASP Core Rule Set activado y lo encuentras en la sección de seguridad, en las llamadas «Conjunto de reglas administradas de Cloudflare» donde solo tienes que comprobar que esté en «On» y con nivel «Medium» o «High», porque con «Low» se cuelan demasiadas cosas. El plan Pro añade reglas gestionadas afinadas para WordPress.
- Pros: El filtrado lo gestiona Cloudflare con un equipo dedicado que actualiza las firmas constantemente y tú no tocas nada, los patrones nuevos se aplican solos.
- Contras: Ningún WAF gestionado pilla todo, sobre todo los XSS basados en DOM, porque el patrón malicioso no viaja en la petición HTTP, y los XSS específicos de un plugin concreto pueden no entrar en las firmas genéricas. Esto te quita un 80% del ruido, el otro 20% lo tiene que parar otra cosa.
Reglas personalizadas para tu caso
Las reglas gestionadas funcionan bien para lo común, pero si has detectado un patrón de ataque específico contra tu web te merece la pena crear reglas a medida en Seguridad → Reglas de seguridad, que se aplican antes que las gestionadas.
Una regla útil de partida es bloquear cualquier petición cuya cadena de consulta (query string) contenga las cadenas más típicas de inyección, algo así:
(http.request.uri.query contains "<script") or (http.request.uri.query contains "javascript:") or (http.request.uri.query contains "onerror=") or (http.request.uri.query contains "onload=")
La acción puede ser Block directo o Managed Challenge si prefieres dar opción a falsos positivos.
La gracia de estas reglas es que filtran lo que de verdad te ataca a ti, pero el coste es que tienes que revisar los logs cada cierto tiempo para detectar patrones nuevos y ajustar.
Cabeceras de seguridad desde la CDN
Esta es la jugada que casi nadie aprovecha y que es canela fina, porque puedes añadir o reescribir cabeceras de seguridad desde Cloudflare sin tocar tu servidor ni tu WordPress, y eso te viene de maravilla si tu hosting no te deja modificar cabeceras o quieres aplicar la misma política a varias webs de golpe.
En Cloudflare lo configuras en Reglas → Configuración → Encabezados de respuestas HTTP y activas las cabeceras que te interesen.
La más importante contra XSS es Content-Security-Policy, que le dice al navegador qué scripts puede ejecutar y desde dónde, de manera que aunque un atacante consiga meter un <script> en tu web, si viene de un dominio que no está en tu CSP el navegador se niega a ejecutarlo.
Las complementarias son X-Content-Type-Options con valor nosniff para evitar que el navegador interprete archivos de un tipo distinto del declarado, y Referrer-Policy con strict-origin-when-cross-origin para reducir la información que se filtra a sitios externos.
Si quieres montarte la CSP a medida sin pelearte con la sintaxis, tienes el generador de cabeceras de seguridad de Ayuda WordPress para sacar la cadena correcta con interfaz visual, y una vez la tengas la pegas en la regla de Cloudflare y listo.
La pega de la CSP es que configurarla bien lleva su tiempo, porque si te pasas de estricta rompes Google Analytics, Tag Manager, captchas y la mitad de los plugins que cargan recursos externos.
La estrategia que funciona es empezar con Content-Security-Policy-Report-Only durante una semana o dos, ver qué se rompería en producción real, ajustar y solo entonces pasar a la modalidad de bloqueo.
Modo Bot Fight
La mayoría de los ataques XSS son oportunistas, no dirigidos, y los lanza un bot que prueba miles de URLs con payloads conocidos a ver si pillan algo, así que filtrar bots ya te quita parte del ruido sin afectar a nadie real.
Lo activas en Seguridad → Configuración → Tráfico de bots → Modo Bot Fight y va solo, aunque conviene comprobar que tus servicios automatizados legítimos (monitorización, backups externos, y sobre todo pasarelas de pago) siguen funcionando bien después.
Filtro 2: el hosting como malla intermedia
El hosting tiene un papel en la defensa contra XSS que la mayoría subestima, porque no es donde se filtran las peticiones HTTP (de eso ya se ocupa la CDN) sino donde se cierran los huecos que permiten que un XSS escale a algo peor.
Sigo con SiteGround en los ejemplos porque es el hosting que uso y conozco a fondo, pero los hosting serios para WordPress traen funciones equivalentes en sus paneles.
WAF a nivel servidor
Los hosting de calidad traen mod_security con el OWASP Core Rule Set o un WAF propio que filtra ataques antes de que la petición llegue a PHP, y esto no es algo que tú configures, viene activado por defecto. Lo mejor es que preguntes a tu hosting, y si duda malo, empieza a buscar alteranativas.
SiteGround tiene un WAF propio con reglas escritas por su equipo de seguridad según van apareciendo vulnerabilidades nuevas en el ecosistema WordPress, y casi todos los buenos hosting ofrecen capas equivalentes.
Si tu hosting no te garantiza un WAF a nivel servidor, lo que tienes es un problema de hosting más que de WordPress.
Mantén PHP actualizado
Suena obvio pero te sorprendería saber cuántas webs hay funcionando todavía en PHP 7.2, 7.0 o incluso 5.6, y las versiones antiguas de PHP no reciben parches de seguridad.
La mayoría de XSS son problema de la aplicación y no del lenguaje, pero hay funciones de PHP relacionadas con el manejo de cadenas, la codificación de URLs y el filtrado de entrada que han recibido mejoras en cada versión, así que ejecutar la última versión te ahorra dolores de cabeza y disgustos.
WordPress recomienda PHP 8.3 o superior, y si estás por debajo cambia ya, que en la mayoría de hosting se hace desde el panel en dos clics.
Aislamiento entre cuentas
En hosting compartidos malos una web vulnerable puede contaminar a las vecinas en el mismo servidor, así que los hosting profesionales aíslan cada cuenta para que un compromiso en una web no afecte al resto.
SiteGround y otros lo llaman aislamiento de cuentas, y otros tienen nombres parecidos. No tienes que configurar nada, solo comprobar que tu hosting lo ofrece, y si no es razón suficiente para cambiarte.
El plugin de seguridad del hosting
Algunos hosting tienen su propio plugin de seguridad para WordPress, SiteGround tiene Security Optimizer, que se puede usar en cualquier hosting y no solo en SiteGround, otros no lo sé, pero pregunta.
Este tiene 2FA, URL de login personalizada, registro de actividad y ocultación de la versión de WordPress, aunque no incluye WAF de aplicación propio, así que conviene combinarlo con un plugin que sí lo traiga o con un WAF en CDN.
Y como siempre, si ya usas otro plugin de seguridad con WAF no instales dos a la vez, porque suelen pelearse.
Filtro 3: WordPress, el colador más fino
Esta es la capa donde más control tienes y la que más afecta a la defensa contra XSS específicos de WordPress, porque las capas anteriores frenan el ruido y los patrones conocidos, pero los XSS específicos de un plugin o un tema concreto solo los frenas desde aquí.
Mantén todo actualizado, esta es la regla número uno
La gran mayoría de XSS que afectan a WordPress vienen de plugins o temas con vulnerabilidades conocidas, parcheadas en versiones posteriores, en webs donde nadie actualiza desde hace meses. Si llevas las actualizaciones al día te ahorras el 80% del problema antes de que exista.
El plan es sencillo y se resume en tener actualizaciones automáticas activadas para todo, core, plugins y temas, sustituir cualquier plugin que lleve más de seis meses sin actualizar, y prescindir sí o sí de los que llevan más de un año o cuyo autor no responde en el foro de soporte.
El caso de Twenty20 es exactamente esto, un plugin abandonado con una vulnerabilidad reportada en WPScan desde hace tiempo, que solo se puede usar parchándolo por tu cuenta o sustituyéndolo por otro.
Lo que mucha gente no sabe es que ni siquiera los plugins de seguridad más populares son inmunes a estos problemas. Wordfence, con 5 millones de instalaciones activas, tiene 13 vulnerabilidades reportadas en WPScan, varias de XSS en su propio código en 2014, 2018 y 2022.
Las han ido parcheando en cada versión, pero si no actualizas el plugin de seguridad, ni el propio plugin de seguridad te protege, así que la actualización no es opcional ni accesoria, es la base de todo lo demás.
Plugin de seguridad con WAF de aplicación
Un plugin de seguridad con cortafuegos dentro de WordPress es la última red para todo lo que se cuela por las capas anteriores. Cuando entras en este terreno hay varias opciones gratuitas decentes, y la elección depende menos de cuál es mejor en abstracto y más de qué encaja con tu caso.
- All-In-One Security (AIOS) es muy completo, con reglas PHP firewall específicas contra XSS, las reglas 6G de Perishable Press, 2FA con integración nativa para WooCommerce, Elementor Pro y bbPress, smart 404 y audit log. Su cortafuegos depende mucho de
.htaccessy en Nginx pierde parte de su fuerza, así que tenlo en cuenta si tu hosting es Nginx puro. - Wordfence es el más popular, y trae WAF con base de firmas amplia gestionada por su equipo, que además es CNA y asigna identificadores CVE. En la versión gratuita las firmas nuevas se aplican con 30 días de retraso respecto a la Premium, ese es el modelo de negocio, pero como cortafuegos gratuito sigue cumpliendo relativamente bien. Yo tengo una mala opinión por otras cosas, pero tú decides.
- Vigilante es 100% gratis sin opciones de pago, con WAF de aplicación que filtra patrones XSS, SQLi y LFI, CSP visual, 2FA por correo y aplicación, escáner de integridad de archivos, modo bajo ataque y registro de auditoría de actividad. Es el que yo desarrollo y mantengo activamente, y que utilizo en todas mis. webs y las de mis clientes, está pensado para tener todo en un solo plugin sin pagar por nada. Como es nuevo todavía tiene poca base instalada, ese es su único pero (de momento).
Los tres bloquean payloads XSS conocidos a nivel de aplicación, y cuál elegir depende de si quieres todo en uno sin pagar (Vigilante o AIOS), si prefieres la base de firmas más amplia aceptando pagar por tenerla al día (Wordfence Premium), o si tu hosting es Apache y quieres aprovechar a tope las reglas .htaccess (AIOS).
Cabeceras de seguridad y Content Security Policy
La CSP es la cabecera que más frena los XSS con diferencia, porque aunque un atacante consiga meter un <script> en tu base de datos, si tu CSP solo permite scripts de tu propio dominio ese script externo no se ejecuta.
Si ya configuras las cabeceras desde Cloudflare en el filtro 1 no hace falta repetirlas aquí, sería duplicar trabajo, pero si no usas CDN o tu CDN no te deja tocar cabeceras, gestiónalas desde el plugin de seguridad.
Vigilante trae ajustes precisos para CSP, AIOS también gestiona cabeceras, y Security Optimizer las trae configuradas por defecto con valores razonables.
El generador de cabeceras de Ayuda WordPress te vale igual para sacar la cadena CSP a medida y pegarla donde te convenga, sea en el plugin, en el .htaccess o en una regla de Cloudflare.
2FA en todas las cuentas con permisos
Esta es defensa en profundidad pura, porque si un XSS consigue robar la cookie de sesión de un administrador, el atacante todavía no podrá iniciar sesión nueva en otro dispositivo sin el segundo factor, así que el daño queda limitado a la sesión activa que pueda exprimir antes de que la víctima cierre el navegador.
Activa la identificación en dos pasos para administradores, editores y autores, que es la gente con permisos suficientes para meterte un disgusto.
La mayoría de plugins de seguridad la traen integrada, pero si prefieres un plugin específico el más completo gratuito es Two-Factor.
HTML controlado en comentarios y formularios
WordPress filtra los comentarios por defecto con wp_kses, pero algunos plugins de comentarios o formularios desactivan ese filtrado para permitir formato enriquecido, y esto es mala idea casi siempre.
Si tienes que permitir HTML en comentarios, restringe las etiquetas a las imprescindibles (<p>, <a>, <b>, <i>) y no permitas nunca <script>, <iframe> ni <object>.
Y en plugins de formularios de contacto o captura de leads, comprueba que los datos del usuario se filtran antes de mostrarse en el panel de admin, porque si tu CRM o tu plugin de formularios pinta el último envío con un avance en el escritorio sin filtrar, ahí entra perfectamente un XSS almacenado dirigido contra ti.
Si tocas código hay tres reglas básicas
Si desarrollas plugins, temas personalizados o snippets que pegas en functions.php, tienes responsabilidad directa en la defensa contra ataques XSS, y las reglas son las mismas que pone la documentación oficial de WordPress.
- Validar todo lo que entra, comprobando antes de hacer nada que el dato del usuario tiene el formato esperado, así que si esperas un número validas que es número, si esperas un email validas el email, y si esperas un valor de una lista cerrada (por ejemplo
horizontalovertical) rechazas cualquier otra cosa que no esté en la lista. - Sanear todo lo que guardas, pasando los datos por la función adecuada según el tipo, con
sanitize_text_field()para texto plano,sanitize_email()para emails,esc_url_raw()para URLs que van a la base de datos, ywp_kses_post()owp_kses()con una lista blanca de etiquetas cuando necesitas permitir algo de HTML. - Escapar todo lo que sacas, en cada sitio donde pintas algo en pantalla, usando la función adecuada según el contexto, con
esc_html()para texto dentro de etiquetas,esc_attr()para valores de atributos,esc_url()para URLs yesc_js()para datos dentro de JavaScript inline.
El parche que escribí para el plugin Twenty20 abandonado, de nuevo, es el ejemplo perfecto de cómo aplicar las tres reglas en un caso real, porque el shortcode no validaba ni saneaba sus atributos y enganché un hook que los procesa antes de pasarlos al plugin original. Quedaba así:
// IDs de imagen: solo números o URLs válidas
$atts['img1'] = is_numeric( $atts['img1'] )
? absint( $atts['img1'] )
: esc_url_raw( $atts['img1'] );
// Offset: decimal entre 0 y 1
$atts['offset'] = max( 0, min( 1, floatval( $atts['offset'] ) ) );
// Orientación: solo valores permitidos
$atts['orientation'] = in_array( $atts['orientation'], array( 'horizontal', 'vertical' ), true )
? $atts['orientation']
: 'horizontal';
// Textos: saneados como texto plano
$atts['before_label'] = sanitize_text_field( $atts['before_label'] );
$atts['after_label'] = sanitize_text_field( $atts['after_label'] );
Cada atributo se valida según lo que se espera de él, así que los IDs van como números, el offset como decimal entre 0 y 1, la orientación como uno de dos valores fijos, y los textos pasan por el saneador estándar de WordPress.
Eso convierte un payload tipo before="<script>alert(1)</script>" en texto plano inofensivo. Tienes el parche completo en el artículo dedicado y el código en mi repositorio de GitHub por si te interesa curiosear.
Qué hago si ya están explotando un XSS en mi web
Si has detectado un XSS activo porque ves JavaScript raro en el código fuente, Google te ha marcado la web como peligrosa o algún visitante te ha avisado de que la web le redirige a sitios extraños, esto es lo que tienes que hacer, en orden, desde fuera hacia dentro.
Primero activa el modo bajo ataque en la CDN, que en Cloudflare lo tienes en el panel principal del dominio. Cualquier visitante pasa un desafío JavaScript antes de cargar la web y el ataque deja de propagarse mientras tú investigas. No es agradable para los visitantes legítimos, pero priorizas contener el daño.
A continuación localiza el origen del XSS. Si es reflejado viene por algún parámetro de URL, así que mira los logs del hosting buscando peticiones con cadenas raras, y si es almacenado está en tu base de datos, y en este caso busca en las tablas wp_posts, wp_postmeta, wp_options y wp_comments cualquier ocurrencia de <script, javascript:, onerror= u onload=. Donde encuentres algo en un campo que no debería tener HTML, ese es tu XSS.
Una vez localizado el origen, sea un plugin que no escapaba un parámetro o un tema personalizado que pintaba un custom field sin filtrar, desactiva al culpable desde el panel o renombrando la carpeta por FTP si te bloquea el acceso.
El tercer paso solo aplica si era XSS almacenado, y consiste en limpiar la base de datos, porque parchear el plugin no basta, el JavaScript malicioso sigue guardado y se ejecutará la próxima vez. Con WP-CLI lo haces así, primero en seco para ver qué afecta y luego en serio:
wp search-replace '<script>...código malicioso...</script>' '' --all-tables --dry-run
Cuando estés seguro de lo que va a cambiar quita el --dry-run y ejecútalo de verdad. Si no te apañas WP-CLI, lo mismo se hace con phpMyAdmin buscando el patrón en cada tabla afectada.
Primero localizas dónde está el patrón con la pestaña «Buscar» al nivel de base de datos (no de tabla). Entras en tu base de datos, pulsas en Buscar, pegas el código malicioso en el campo de búsqueda, marcas la frase exacta y seleccionas todas las tablas. Esto te dice en qué tablas y cuántas filas están afectadas sin tocar nada todavía.
Cuando ya sabes qué tablas tienen el problema, haces una copia de seguridad de la base de datos y pasas a la pestaña SQL para ejecutar el reemplazo real con la función REPLACE de MySQL. Las tablas habituales donde se aloja XSS almacenado son wp_posts, wp_postmeta, wp_options y wp_comments (ojo si tu prefijo no es wp_).
Para contenido de entradas y páginas:
UPDATE wp_posts SET post_content = REPLACE(post_content, '<script>...código malicioso...</script>', '') WHERE post_content LIKE '%<script>...código malicioso...%';
Para comentarios:
UPDATE wp_comments SET comment_content = REPLACE(comment_content, '<script>...código malicioso...</script>', '') WHERE comment_content LIKE '%<script>...%';
El WHERE no es imprescindible pero limita el UPDATE a las filas que realmente contienen el patrón, así evitas reescribir toda la tabla sin necesidad. phpMyAdmin te mostrará cuántas filas se han modificado al ejecutar la consulta, que es lo más parecido a la confirmación que da WP-CLI.
Precaución importante con los datos serializados: Las tablas
wp_options(campooption_value) ywp_postmeta(campometa_value) suelen contener arrays serializados de PHP, donde la longitud de cada string forma parte del propio dato. Si haces un REPLACE directo y el script malicioso está dentro de un valor serializado, vas a romper la serialización y dejar la opción o el meta inservibles. Aquí WP-CLI gana por goleada porque sabe deserializar y volver a serializar al vuelo. Si el patrón aparece enwp_optionsowp_postmetay no puedes usar WP-CLI, lo más seguro es editar manualmente cada fila afectada desde phpMyAdmin ajustando el contenido sin romper la estructura.
Un detalle con las comillas es que si el JavaScript malicioso tiene comillas simples dentro, escápalas con barra invertida (\') o cambia los delimitadores de la cadena SQL a comillas dobles.
El cuarto paso es forzar el restablecimiento de contraseñas de todos los administradores, porque si el XSS pudo robar cookies de sesión tienes que asumir que tu sesión está comprometida.
La mayoría de plugins de seguridad (los buenos) traen una herramienta de restablecer contraseñas para todos los usuarios que invalida sesiones activas y obliga a iniciar sesión de nuevo, y aprovecha el momento para activar 2FA si no lo tenías.
Para finalizar revisa los logs y aprende del incidente. Mira el registro de actividad de tu plugin de seguridad para entender cuándo se metió el XSS, qué usuario o IP lo coló y qué pasó después, porque esa información te sirve para crear reglas nuevas en la CDN y reforzar la defensa contra el siguiente intento.
Errores típicos que cometemos todos al tratar con ataques XSS
Te lo digo desde la experiencia (mala), que es de lo que mejor se aprende, luego tú decides:
- Confiar solo en el plugin de seguridad, que es la última capa y no la primera, porque si todo el peso recae en él cada ataque consume recursos de PHP antes de que pueda filtrarlo.
- No usar CDN con WAF, cuando Cloudflare gratuito ya filtra patrones XSS conocidos y es la inversión cero más rentable que existe en seguridad WordPress.
- No mantener todo actualizado, porque la mayoría de XSS que afectan a WordPress vienen de plugins desactualizados con vulnerabilidades parcheadas en versiones posteriores, así que actualizar te ahorra el 80% del problema.
- Usar plugins abandonados, como el caso Twenty20, porque si el autor no actualiza no hay parches y las vulnerabilidades persisten para siempre. Busca alternativas o aplica parches comunitarios.
- Permitir HTML libre en comentarios o formularios, cuando casi nunca lo necesitas y siempre puedes restringir las etiquetas con
wp_ksesa las imprescindibles. - No filtrar campos personalizados en plantillas a medida, porque si tu tema imprime un campo personalizado en la parte pública de la web sin pasar por
esc_html()cualquiera con permisos para editar ese campo puede meterte scripts. - Asumir que muchas instalaciones equivale a seguridad, cuando plugins con cientos de miles de instalaciones aparecen en WPScan con XSS reportados todos los meses y la popularidad no es garantía de nada.
- Configurar mal la CSP, porque una CSP demasiado laxa con
unsafe-inlineounsafe-evalno defiende contra XSS y solo da sensación de hacerlo, mientras que una demasiado estricta rompe la web. La estrategia que funciona es empezar en modo Report-Only y refinar antes de poner en bloqueo. - No tener copias de seguridad, porque si todo falla y un XSS escala a compromiso total del WordPress, una copia limpia es lo único que te salva.
El plan completo en plan chuleta
Aquí tienes el resumen aplicable de todo lo visto, de fuera hacia dentro, para que lo configures en orden y en una tarde.
Capa 1: CDN
| Acción | Prioridad | Herramienta | Impacto en usuario |
|---|---|---|---|
| WAF gestionado con OWASP CRS activo | Alta | Cloudflare → Seguridad → Reglas de seguridad | Ninguno |
| Reglas personalizadas contra patrones XSS | Alta | Cloudflare → Reglas de seguridad | Mínimo |
| Cabeceras de seguridad y CSP | Alta | Cloudflare → Reglas → Configuración → Encabezados de respuestas HTTP | Ninguno bien configurada |
| Modo Bot Fight | Media | Cloudflare → Seguridad → Bots | Ninguno |
| Modo Under Attack | Emergencias | Cloudflare → Escritorio | Alto, todos pasan desafío |
Capa 2: Hosting
| Acción | Prioridad | Herramienta | Impacto en usuario |
|---|---|---|---|
| WAF a nivel servidor activo | Alta | Hosting, viene activado | Ninguno |
| PHP en versión 8.1 o superior | Alta | Panel del hosting | Ninguno |
| Aislamiento de cuentas | Alta | Hosting, debe ofrecerlo | Ninguno |
| Plugin de seguridad del hosting si no hay otro | Media | Security Optimizer o equivalente | Ninguno |
Capa 3: WordPress
| Acción | Prioridad | Herramienta | Impacto en usuario |
|---|---|---|---|
| Actualizaciones automáticas de WordPress, plugins, temas | Crítica | WordPress | Ninguno |
| Plugin de seguridad con WAF de aplicación | Alta | Vigilante, AIOS o Wordfence | Ninguno |
| CSP configurada en plugin si no está en CDN | Alta | Vigilante, AIOS o generador de cabeceras | Ninguno si está bien configurada |
| 2FA para administradores, editores y autores | Alta | Vigilante o Two-Factor | Bajo, un paso más en el login |
| HTML restringido en comentarios y formularios | Alta | Configuración de WordPress | Mínimo |
| Campos personalizados escapados en plantillas | Alta | Código del tema | Ninguno |
| Registro de actividad activo | Media | Vigilante o WP Activity Log | Ninguno |
Seguridad continua
| Acción | Frecuencia |
|---|---|
| Comprobar plugins en WPScan y Patchstack | Semanal |
| Revisar errores JavaScript en consola del navegador | Semanal |
| Pasar el analizador de seguridad externo | Mensual |
| Auditar plugins activos y eliminar los que no se usan | Mensual |
| Sustituir plugins sin actualizar desde hace más de 6 meses | Cuando los detectes |
| Revisar logs del CDN y del hosting | Cuando pase algo raro |
| Comprobar copias de seguridad | Semanal |
Todo lo que ves aquí lo puedes configurar en poco rato, sin gastar dinero en plugins premium ni servicios de pago, porque la diferencia entre una web con XSS explotables y una web protegida no está en cuánto te gastes sino en si tienes los tres filtros montados y trabajando juntos. Si uno falla los otros siguen filtrando, esa es toda la gracia del asunto.
Si llevas tiempo sin revisar la seguridad de tu web empieza pasando el analizador de seguridad de Ayuda WordPress para ver qué tienes expuesto, busca tus plugins activos en WPScan, y monta los tres filtros siguiendo las tablas de arriba.
Cuando termines vuelve a pasar el analizador y verás la diferencia. Y, recuerda, por terminar por donde empecé, si tienes Twenty20 instalado, instálale el parche o sustitúyelo cuanto antes por otro plugin del tipo antes-después de los muchos que hay, que no es el único.
¿Dudas, abrazos, insultos? lo que quieras ahí abajo, en los comentarios.
¿Te gustó este artículo? ¡Ni te imaginas lo que te estás perdiendo en YouTube!






