Qué es Cross-Site Scripting (XSS) y cómo prevenirlo

Cross-Site Scripting (XSS) es una vulnerabilidad que permite que contenido controlado por un atacante termine interpretándose como código en el navegador de otra persona. El código se ejecuta dentro del contexto de un sitio legítimo, por lo que puede acceder a la información y a las acciones que el navegador tenga disponibles para ese origen.

El riesgo no depende solamente de que exista un formulario. También puede aparecer en parámetros de una URL, búsquedas, comentarios, perfiles, nombres de archivos, datos obtenidos desde una API o valores que JavaScript lee y escribe en la página.

Qué puede conseguir un ataque XSS

El impacto cambia según la aplicación, los permisos del usuario afectado y las defensas del sitio. Un XSS podría modificar lo que se muestra, realizar acciones con la sesión iniciada, capturar información que la página expone, enviar al usuario a un sitio falso o insertar formularios engañosos.

Las cookies marcadas como HttpOnly no pueden leerse desde JavaScript, pero eso no elimina la vulnerabilidad: el código inyectado todavía podría actuar desde la sesión abierta. Las opciones Secure y SameSite también son capas útiles, no una corrección para XSS.

Tipos de XSS más habituales

  • Almacenado: el contenido malicioso queda guardado, por ejemplo en un comentario o perfil, y se entrega a quienes visitan la página afectada.
  • Reflejado: la aplicación incorpora en su respuesta un valor recibido en la solicitud, como un parámetro de búsqueda, sin tratarlo correctamente.
  • Basado en DOM: el problema se produce en el navegador cuando JavaScript toma un dato inseguro y lo envía a una función o propiedad capaz de interpretarlo como HTML o código.

Esta clasificación ayuda a investigar el origen, aunque la pregunta más importante es siempre la misma: ¿de dónde proviene el dato y en qué contexto termina utilizándose?

La defensa principal: tratar la salida según su contexto

Escapar o codificar la salida convierte caracteres especiales en una representación que el navegador interpreta como texto. La técnica correcta depende del lugar donde se inserta el valor: contenido HTML, atributo HTML, componente de una URL, CSS o JavaScript no comparten las mismas reglas.

No alcanza con aplicar una función genérica a todos los casos. Un valor preparado para mostrarse entre etiquetas puede seguir siendo peligroso dentro de un atributo o de un bloque JavaScript. Conviene utilizar las funciones de escape que ofrece el framework o CMS específicamente para cada contexto y mantener activado el autoescape de las plantillas.

Preferir destinos seguros en JavaScript

Cuando el objetivo es mostrar texto, propiedades como textContent son más seguras que innerHTML, porque no interpretan el contenido como marcado. También conviene evitar document.write, la construcción de manejadores de eventos a partir de texto y funciones que ejecutan cadenas como código.

Si la aplicación realmente necesita admitir HTML generado por usuarios, por ejemplo en un editor enriquecido, no debe tratarlo como texto confiable. Es necesario pasarlo por un sanitizador mantenido, con una lista limitada de elementos y atributos permitidos. Después de sanitizarlo, no hay que volver a concatenarle contenido inseguro.

Validar entradas ayuda, pero no reemplaza el escape

La validación permite rechazar valores que no corresponden al dato esperado: una fecha con formato inválido, un identificador con caracteres extraños o una opción que no figura entre las permitidas. Esto mejora la seguridad y la calidad de los datos, pero no sustituye el tratamiento correcto al mostrar el valor.

Las listas permitidas son preferibles cuando el conjunto válido es conocido. Aun así, un nombre, una dirección o un comentario pueden contener caracteres legítimos que requieren escape. Intentar detectar únicamente fragmentos como etiquetas o palabras sospechosas suele producir bloqueos incorrectos y deja variantes sin cubrir.

Evitar contextos especialmente peligrosos

Siempre que sea posible, no insertes datos variables directamente dentro de código JavaScript, nombres de etiquetas, nombres de atributos, comentarios HTML o atributos de eventos como onclick. En esos lugares es fácil cometer errores incluso aplicando transformaciones.

Una alternativa más clara es entregar los datos como JSON con serialización segura, almacenarlos en atributos diseñados para datos y leerlos con APIs del navegador que no ejecuten contenido. Separar estructura, datos y comportamiento reduce las rutas por las que una entrada puede llegar a un destino peligroso.

Content Security Policy como capa adicional

Una política Content Security Policy (CSP) puede limitar desde qué orígenes se cargan scripts y dificultar la ejecución de código insertado. Sin embargo, no reemplaza la corrección del flujo vulnerable. Una política demasiado amplia, con scripts inline o comodines innecesarios, ofrece poca protección.

Para implementarla sin romper el sitio, conviene comenzar con modo de reporte, revisar los recursos legítimos y luego aplicar una política basada en orígenes estrictos, nonces o hashes cuando la arquitectura lo permita. Los reportes también necesitan filtrado: una alerta por sí sola no demuestra un ataque exitoso.

WordPress, plugins y otros CMS

En WordPress y otros CMS, muchas fallas XSS se corrigen actualizando el núcleo, temas o extensiones. Eliminá componentes que no se usan, evitá plugins sin mantenimiento y asigná solamente los permisos necesarios a cada cuenta. Un XSS accesible únicamente a un administrador puede seguir siendo grave si una cuenta privilegiada visita contenido preparado por un atacante.

Un WAF como ModSecurity puede bloquear ciertos patrones y ganar tiempo, pero no conoce todos los contextos de la aplicación. Consultá también qué es ModSecurity y cómo funciona como firewall de aplicaciones web.

Cómo revisar una aplicación

  1. Inventariá todas las entradas: formularios, URL, cabeceras, archivos, APIs y datos almacenados.
  2. Seguí cada dato hasta el lugar donde se presenta o utiliza en el navegador.
  3. Confirmá que la plantilla escape automáticamente y que cualquier excepción esté justificada.
  4. Buscá destinos peligrosos en JavaScript y reemplazalos por APIs que trabajen con texto.
  5. Probá distintos perfiles, incluidas cuentas con permisos limitados y administradores.
  6. Revisá dependencias del frontend y aplicá sus actualizaciones de seguridad.

El análisis debe complementarse con la protección de formularios y sesiones. Nuestra guía sobre cómo proteger un formulario web contra spam y abuso cubre controles adicionales que no deben confundirse con la prevención de XSS.

Qué hacer si detectás una vulnerabilidad

Corregí el punto donde el dato llega al destino inseguro y buscá otras rutas que reutilicen el mismo componente. Actualizá o deshabilitá temporalmente el plugin afectado, preservá logs, revisá usuarios y acciones administrativas, y cerrá sesiones si existe riesgo de uso indebido.

Después de aplicar la solución, repetí las pruebas y comprobá que el contenido legítimo siga funcionando. Bloquear una URL o una cadena concreta no es suficiente: la prevención sólida combina codificación contextual, sanitización cuando corresponde, APIs seguras, software actualizado y una CSP bien diseñada como defensa complementaria.