Emtix

Cross-Site Scripting (XSS)

Publicado el 7 de diciembre de 2025

Introducción

El Cross-Site Scripting (XSS) sigue siendo una de las vulnerabilidades más comunes y peligrosas en aplicaciones web. Según el OWASP Top 10, las vulnerabilidades XSS continúan afectando a innumerables sitios y aplicaciones, lo que puede exponer a los usuarios a robo de datos, secuestro de sesión y distribución de malware.

¿Sabías qué?

XSS ha aparecido de forma constante en el OWASP Top 10 desde su creación, lo que demuestra la persistencia de esta clase de vulnerabilidades a pesar del aumento en la concientización.

Esta guía te ayudará a entender cómo identificar vulnerabilidades XSS durante una revisión de código, explicar cómo los atacantes explotan estas debilidades y darte técnicas prácticas para prevenirlas.

¿Qué es XSS?

Cross-Site Scripting (XSS) es una vulnerabilidad que permite a un atacante inyectar scripts maliciosos en páginas web que son vistas por otros usuarios.

Estos scripts se ejecutan en el navegador de la víctima y pueden acceder a cookies, tokens de sesión y otra información sensible que el navegador conserva para ese sitio.

Los ataques XSS ocurren cuando una aplicación incluye datos no confiables dentro de una página web sin aplicar una validación o un escape adecuado.

Tipos de vulnerabilidades XSS

TipoDescripciónVector de ataque
XSS ReflejadoEl script se refleja desde el servidor web, por ejemplo, en resultados de búsqueda o mensajes de error.Parámetros en la URL, campos de formularios
XSS AlmacenadoEl script malicioso se almacena en el servidor objetivo, como en una base de datos.Comentarios, perfiles de usuario, foros
XSS basado en DOMLa vulnerabilidad existe en el código del lado del cliente en lugar del lado del servidor.Fragmentos de URL, almacenamiento del lado del cliente

XSS Reflejado

El vector de ataque

El atacante identifica un parámetro en la URL (como una búsqueda o un mensaje de error) que la página muestra en pantalla sin validar.

Construye una URL especial que contiene código JavaScript y engaña a la víctima para que haga clic en ella (Ingeniería Social).

Procesamiento en el Servidor

El servidor recibe la petición con el código malicioso en la URL. Al generar el HTML de respuesta, toma ese parámetro y lo concatena directamente en el código fuente de la página.

El servidor no guarda nada en base de datos; el ataque solo existe durante el ciclo petición-respuesta.

Ejecución en el Cliente

El navegador de la víctima recibe el HTML. Al encontrar las etiquetas de script que venían en la URL, las interpreta como código ejecutable legítimo y realiza la acción maliciosa (ej. enviar cookies al atacante).

Paso 1: El Payload en la URL

Paso 2: La Reflexión

El servidor devuelve este HTML al navegador:

<p>Resultados para: <script>alert(1)</script></p>

XSS Almacenado

Inyección Persistente

El atacante utiliza un formulario legítimo (comentarios, perfil de usuario, foro) para enviar código JavaScript en lugar de texto normal.

A diferencia del XSS Reflejado, el atacante no necesita enviar un enlace a la víctima; solo necesita que la víctima visite la página infectada.

Persistencia de Datos

El código malicioso se almacena en la base de datos del servidor como si fuera contenido válido (ej: un comentario de blog).

Esto hace que el ataque sea permanente hasta que se limpie la base de datos.

Ejecución Masiva

Cualquier usuario que cargue la página donde se muestra el comentario infectado ejecutará el script automáticamente.

Es considerado de alto riesgo porque puede afectar a miles de usuarios sin interacción directa con el atacante.

XSS basado en DOM

Vector de Ataque: Fragmento URL

El atacante construye una URL que incluye el código malicioso después del símbolo de almohadilla (#).

Según el estándar HTTP, los navegadores no envían el contenido del fragmento al servidor web. Este dato permanece exclusivamente en el cliente.

Servidor: Procesamiento Estándar

El servidor web recibe una petición limpia (GET /pagina) sin el código del atacante.

El servidor responde devolviendo el código HTML y JavaScript legítimo de la aplicación. No hay filtros de seguridad en el servidor que puedan detectar este ataque porque el payload nunca llega a él.

Source (Fuente de Datos)

El código JavaScript legítimo de la página se ejecuta en el navegador. Este script lee datos de una propiedad del navegador (el “Source”).

En este ataque, el script lee location.hash (el fragmento de la URL) asumiendo que es un dato seguro, pero en realidad contiene el código inyectado por el atacante.

Sink (Punto de Ejecución)

El “Sink” es una función o propiedad del DOM que renderiza o ejecuta contenido.

El ataque se materializa cuando el script toma los datos del Source y los asigna al Sink (ej. innerHTML) sin validación previa, provocando que el navegador interprete y ejecute el payload malicioso inmediatamente.

Quiz

¿Cuál de las siguientes opciones describe mejor el riesgo principal de las vulnerabilidades XSS?


Sources & Sinks

Source

Un source es cualquier propiedad, función o punto del código desde donde se obtiene información que puede ser controlada por un atacante. Los sources son el origen del flujo de datos no confiables.

Ejemplos típicos:

  • location.search (lee parámetros de la URL)
  • document.referrer (lee la URL de referencia)
  • document.cookie (accede a cookies manipulables por el usuario)
  • Mensajes recibidos mediante postMessage
  • Valores almacenados en localStorage o sessionStorage

Cualquier mecanismo que permita que un atacante influya en el valor leído por el código puede considerarse un source.


Sink

Un sink es una función, propiedad u operación que puede producir efectos peligrosos si recibe datos controlados por un atacante. Los sinks son el punto donde el flujo de datos se vuelve riesgoso.

Ejemplos típicos de sinks:

  • Ejecución de JavaScript

    • eval()
    • setTimeout() cuando recibe una cadena
    • new Function()
  • Manipulación del DOM con HTML

    • element.innerHTML
    • element.outerHTML
    • document.write()
  • Atributos que permiten ejecutar código o cargar recursos

    • element.src
    • element.href si acepta esquemas como javascript:

Un sink se convierte en una vulnerabilidad cuando recibe datos que provienen de un source sin validación ni saneamiento.

Relación entre source y sink

  • El source proporciona el dato que puede estar bajo control del atacante.
  • El sink es la operación peligrosa que se ejecuta con ese dato.
  • Entre ambos puede existir o no un proceso de validación o sanitización.
  • Si el dato fluye de un source a un sink sin protección, existe una vulnerabilidad.

Sinks de XSS comunes

JavaScript (NodeJS y navegador)

// DOM manipulation sinks
element.innerHTML = userInput;
element.outerHTML = userInput;
element.insertAdjacentHTML('beforeend', userInput);
document.write(userInput);
document.writeln(userInput);

// JavaScript execution sinks
eval(userInput);
setTimeout(userInput, 100);
setInterval(userInput, 100);
new Function(userInput);

// HTML attribute sinks
element.setAttribute('src', userInput);
element.setAttribute('href', userInput);
element.setAttribute('data', userInput);
element.src = userInput;
element.href = userInput; // cuidado con "javascript:"
element.onclick = userInput; // también cualquier on<Event>

Java (Servlets / JSP)

// HTML output sinks
out.print(userInput);
out.write(userInput);
response.getWriter().write(userInput);

// Attribute sinks (en etiquetas JSP o HTML manual)
"<a href=\"" + userInput + "\">enlace</a>"
"<img src=\"" + userInput + "\"/>"

// JavaScript execution via templates or inline
"<script>" + userInput + "</script>"

Python (Flask, Django sin escape)

# HTML output sinks
return userInput
response = make_response(userInput)

# Template injection (Jinja2/Django con autoescape desactivado)
{{ userInput|safe }}
{{ userInput|escape(False) }}

# Attribute injection
"<a href='{}'>".format(userInput)
f"<img src='{userInput}'>"

PHP

// Output sinks
echo $_GET['param'];
print $_POST['data'];

// HTML embedding
echo "<div>" . $_REQUEST['name'] . "</div>";
printf("<a href='%s'>", $_GET['link']);

// JavaScript injection
echo "<script>" . $_GET['code'] . "</script>";

C# (.NET - MVC, Razor)

// HTML output without encoding
@Html.Raw(userInput)
Response.Write(userInput);
LiteralControl.Text = userInput;

// HTML attribute injection
"<img src='" + userInput + "' />"
"<a href='" + userInput + "'>link</a>"

// JavaScript embedding
"<script>" + userInput + "</script>"

Rastreo del Flujo de Datos (Tracing Data Flow)

Para encontrar vulnerabilidades de XSS durante una revisión de código, debes revisar cómo fluye la información desde sources hasta sinks. Esto implica seguir la entrada del usuario conforme se mueve a través de la aplicación.

Análisis de Contaminación (Taint Analysis)

Una técnica llamada taint analysis permite rastrear entrada potencialmente peligrosa dentro de la base de código. Las herramientas modernas de análisis estático (SAST) pueden automatizar este proceso siguiendo variables derivadas de datos controlados por el usuario.

Preguntas clave en una revisión de código:

  • ¿De dónde proviene la entrada del usuario?
  • ¿Cómo se transforma esa entrada a medida que fluye por el código?
  • ¿Se valida o sanitiza correctamente antes de llegar a un sink?
  • ¿Existen bypasses o casos límite que podrían evadir las protecciones?

Las aplicaciones modernas a menudo tienen flujos de datos complejos que abarcan múltiples archivos, funciones e incluso servicios. Herramientas como SAST (Static Application Security Testing) pueden ayudar a identificar flujos potenciales desde sources hacia sinks.

Técnicas de Prevención

Prevenir XSS requiere combinar buenas prácticas de desarrollo con controles de seguridad.

Defensa en Profundidad (Defense in Depth)

La estrategia más efectiva para prevenir XSS combina múltiples capas de defensa. Ningún mecanismo individual es infalible.

Incluye:

  • Validación de entrada: validar contra listas blancas de valores o patrones permitidos.
  • Codificación de salida (Output Encoding): codificar caracteres especiales antes de insertar datos en contextos HTML, JavaScript, CSS o URL.
  • Content Security Policy (CSP): usar cabeceras HTTP para restringir qué scripts pueden ejecutarse.
  • APIs seguras: preferir APIs que no interpreten HTML/JS, como textContent en lugar de innerHTML.
  • Librerías de sanitización: usar librerías probadas para limpiar HTML o contenido del usuario.

Codificación Sensible al Contexto (Context-Sensitive Encoding)

// HTML context encoding
const safeHtml = escapeHtml(userInput);
element.innerHTML = safeHtml;

// JavaScript context encoding
const safeJs = JSON.stringify(userInput);
element.onclick = function () { alert(safeJs); };

// URL context encoding
const safeUrl = encodeURIComponent(userInput);
element.href = 'https://example.com/search?q=' + safeUrl;

// CSS context encoding
const safeCss = escapeCss(userInput);
element.style = 'color: ' + safeCss;

Corregir el Código Vulnerable

function showProfile(username) {
  // Display the username in the profile section
  document.getElementById('profile-name').innerHTML = username;
}
Quiz

¿Cuál es la mejor solución?

Cuestionario: Métodos de Prevención de XSS

Quiz

¿Qué vulnerabilidad ayuda a mitigar Content Security Policy (CSP)?

Quiz

¿Qué vulnerabilidad ayuda a mitigar el uso de cookies con bandera HttpOnly?

Quiz

¿Qué vulnerabilidad ayuda a mitigar la sanitización HTML?

Quiz

¿Qué vulnerabilidad ayuda a mitigar el JavaScript encoding?

Recomendaciones por Framework

Los frameworks modernos proveen protecciones integradas contra XSS, pero solo funcionan si se usan correctamente.

FrameworkPatrones segurosPatrones inseguros (sinks)
ReactJSX con {} (autoescape)dangerouslySetInnerHTML
AngularInterpolación {{ }}[innerHTML], bypassSecurityTrustHtml
Vue{{ }}, v-textv-html
Next.jsJSX / Server ComponentsdangerouslySetInnerHTML
ExpressMotores de plantilla con autoescapeConstruir HTML manualmente en res.send()

Consejos rápidos:

  • React: usar JSX; evitar dangerouslySetInnerHTML.
  • Angular: usar property binding seguro; evitar innerHTML.
  • Vue: usar v-text; v-html solo con contenido sanitizado.
  • Node/Express: siempre usar motores de plantillas con autoescape (Pug/EJS/Handlebars).

Desafío interactivo

¿Cuál de estos fragmentos React es vulnerable a XSS?

UserProfile.jsx
1 function UserProfile({ username }) {
2 return <div>{username}</div>;
3 }
4
5 function UserProfileRisk({ username }) {
6 return <div dangerouslySetInnerHTML={{ __html: username }} />;
7 }
8
9 function UserProfileStyled({ username }) {
10 return <div className=class="text-[#a5d6ff]">"user-profile">{username.toUpperCase()}</div>;
11 }
12
13 function UserProfileAnon({ username }) {
14 const displayName = username || class="text-[#a5d6ff]">'Anonymous';
15 return <div>{displayName}</div>;
16 }

Vulnerabilidad (Línea 6)

React escapa automáticamente el contenido de las variables.

Sin embargo, dangerouslySetInnerHTML es un comando explícito para desactivar esa protección e insertar HTML crudo. Si username contiene <script>, se ejecutará.

Da click en la línea 6

Casos Reales

Incluso grandes empresas han sufrido vulnerabilidades XSS.

Caso: Twitter — XSS(2019)

  • Los atacantes podían publicar tweets con JavaScript embebido.
  • Causa: sanitización insuficiente de contenido.

Lección: incluso plataformas maduras necesitan auditorías constantes y sanitización robusta.


Recursos Útiles

Artículos para responder cuestionarios

Análisis estático (SAST):

Escáneres dinámicos (DAST):

Sanitización:

Protección en tiempo de ejecución:

Recursos para aprender más sobre XSS:


Ejercicios

Ejercicio 1: Kotlin & Spring Boot XSS

Esta aplicación permite a los usuarios previsualizar sus publicaciones formateadas con Markdown antes de publicarlas. Sin embargo, un atacante ha descubierto que puede ejecutar JavaScript arbitrario en la vista de previsualización.

Analiza la arquitectura completa y encuentra la línea exacta en el servicio de Markdown que introduce esta vulnerabilidad.

PostController.kt
1 package com.forumboards.controllers
2
3 import com.forumboards.models.Post
4 import com.forumboards.services.MarkdownService
5 import com.forumboards.services.PostService
6 import org.springframework.stereotype.Controller
7 import org.springframework.ui.Model
8 import org.springframework.web.bind.annotation.*
9
10 @Controller
11 @RequestMapping(class="text-[#a5d6ff]">"/posts")
12 class PostController(private val postService: PostService,
13 private val markdownService: MarkdownService) {
14
15 @GetMapping
16 fun getAllPosts(model: Model): String {
17 model.addAttribute(class="text-[#a5d6ff]">"posts", postService.getAllPosts())
18 return class="text-[#a5d6ff]">"post/list"
19 }
20
21 @GetMapping(class="text-[#a5d6ff]">"/preview")
22 fun previewPost(@RequestParam content: String, model: Model): String {
23 model.addAttribute(class="text-[#a5d6ff]">"renderedContent", markdownService.renderMarkdown(content))
24 return class="text-[#a5d6ff]">"post/preview"
25 }
26
27 @PostMapping
28 fun createPost(@ModelAttribute post: Post): String {
29 postService.savePost(post)
30 return class="text-[#a5d6ff]">"redirect:/posts"
31 }
32 }

Vulnerabilidad (Línea 10)

Al configurar .escapeHtml(false), estás permitiendo explícitamente que cualquier etiqueta HTML incluida en el Markdown (como <script>) se renderice tal cual.

Aunque el objetivo sea permitir formato rico, esto abre la puerta a XSS. La solución correcta es habilitar el escape aquí o implementar una librería de sanitización (como Jsoup) antes de devolver el string.