Back

Prevención de XSS en Contenido Generado por Usuarios

Prevención de XSS en Contenido Generado por Usuarios

Los ataques de cross-site scripting (XSS) a través de contenido generado por usuarios siguen siendo una de las amenazas de seguridad más persistentes que enfrentan las aplicaciones web. Ya sea que estés construyendo un sistema de comentarios, manejando envíos de formularios o implementando editores de texto enriquecido, cualquier funcionalidad que acepte y muestre entrada de usuario crea vulnerabilidades potenciales de XSS. Los frameworks modernos de JavaScript proporcionan protecciones integradas, pero sus mecanismos de escape y la complejidad de las aplicaciones del mundo real significan que los desarrolladores deben entender e implementar técnicas adecuadas de prevención de XSS.

Este artículo cubre las estrategias esenciales para prevenir XSS en contenido generado por usuarios: validación y normalización de entrada, codificación de salida consciente del contexto, manejo seguro de contenido enriquecido y controles suplementarios de defensa en profundidad. Aprenderás por qué la validación por lista de permitidos supera al filtrado por lista de denegados y cómo aprovechar los valores predeterminados del framework mientras evitas las trampas de seguridad comunes.

Puntos Clave

  • Siempre valida la entrada de usuario usando listas de permitidos, no listas de denegados
  • Aplica el método de codificación correcto para cada contexto de salida (HTML, JavaScript, CSS, URL)
  • Usa DOMPurify o librerías similares para sanitizar contenido HTML enriquecido
  • Aprovecha los valores predeterminados del framework y evita los mecanismos de escape a menos que sea absolutamente necesario
  • Implementa defensa en profundidad con headers CSP y atributos seguros de cookies
  • Prueba tus medidas de prevención de XSS con pruebas de seguridad automatizadas

Entendiendo los Riesgos de XSS en Contenido Generado por Usuarios

El contenido generado por usuarios presenta desafíos únicos de XSS porque combina entrada no confiable con la necesidad de funcionalidades dinámicas e interactivas. Los sistemas de comentarios, perfiles de usuario, reseñas de productos y herramientas de edición colaborativa requieren aceptar contenido similar a HTML mientras previenen la ejecución de scripts maliciosos.

Los frameworks modernos como React, Angular y Vue.js manejan la prevención básica de XSS automáticamente a través de sus sistemas de plantillas. Sin embargo, estas protecciones se rompen cuando los desarrolladores usan mecanismos de escape del framework:

  • dangerouslySetInnerHTML de React
  • Métodos bypassSecurityTrustAs* de Angular
  • Directiva v-html de Vue
  • Manipulación directa del DOM con innerHTML

Estas características existen por razones legítimas—mostrar contenido formateado, integrar widgets de terceros o renderizar HTML creado por usuarios. Pero cada bypass crea un vector potencial de XSS que requiere manejo cuidadoso.

Validación de Entrada: Tu Primera Línea de Defensa

Implementando Validación por Lista de Permitidos

La validación por lista de permitidos define exactamente qué entrada es aceptable, rechazando todo lo demás por defecto. Este enfoque resulta mucho más seguro que el filtrado por lista de denegados, que intenta bloquear patrones peligrosos conocidos.

Para datos estructurados como direcciones de correo electrónico, números de teléfono o códigos postales, usa expresiones regulares estrictas:

// Validación por lista de permitidos para códigos ZIP de EE.UU.
const zipPattern = /^\d{5}(-\d{4})?$/;

function validateZipCode(input) {
  if (!zipPattern.test(input)) {
    throw new Error('Invalid ZIP code format');
  }
  return input;
}

Por Qué Fallan los Filtros de Lista de Denegados

Los enfoques de lista de denegados que intentan filtrar caracteres peligrosos como <, >, o etiquetas script inevitablemente fallan porque:

  1. Los atacantes fácilmente evaden los filtros usando codificación, variaciones de mayúsculas o peculiaridades del navegador
  2. El contenido legítimo se bloquea (como “O’Brien” cuando se filtran apostrofes)
  3. Nuevos vectores de ataque emergen más rápido de lo que las listas de denegados pueden actualizarse

Normalizando Unicode y Texto de Forma Libre

Para contenido generado por usuarios que incluye texto de forma libre, implementa normalización Unicode para prevenir ataques basados en codificación:

function normalizeUserInput(text) {
  // Normalizar a forma NFC
  return text.normalize('NFC')
    // Remover caracteres de ancho cero
    .replace(/[\u200B-\u200D\uFEFF]/g, '')
    // Recortar espacios en blanco
    .trim();
}

Al validar texto de forma libre, usa listas de permitidos de categorías de caracteres en lugar de intentar bloquear caracteres peligrosos específicos. Este enfoque soporta contenido internacional mientras mantiene la seguridad.

Codificación de Salida Consciente del Contexto

La codificación de salida transforma datos de usuario a un formato seguro para mostrar. La idea clave: diferentes contextos requieren diferentes estrategias de codificación.

Codificación de Contexto HTML

Al mostrar contenido de usuario entre etiquetas HTML, usa codificación de entidades HTML:

function encodeHTML(str) {
  const div = document.createElement('div');
  div.textContent = str;
  return div.innerHTML;
}

// Seguro: el contenido del usuario está codificado
const userComment = "<script>alert('XSS')</script>";
element.innerHTML = `<p>${encodeHTML(userComment)}</p>`;
// Se renderiza como: <p>&lt;script&gt;alert('XSS')&lt;/script&gt;</p>

Codificación de Contexto JavaScript

Las variables colocadas en contextos JavaScript requieren codificación hexadecimal:

function encodeJS(str) {
  return str.replace(/[^\w\s]/gi, (char) => {
    const hex = char.charCodeAt(0).toString(16);
    return '\\x' + (hex.length < 2 ? '0' + hex : hex);
  });
}

// Seguro: los caracteres especiales están codificados en hexadecimal
const userData = "'; alert('XSS'); //";
const script = `<script>var userName = '${encodeJS(userData)}';</script>`;

Codificación de Contexto CSS

Los datos de usuario en CSS requieren codificación específica de CSS:

function encodeCSS(str) {
  return str.replace(/[^\w\s]/gi, (char) => {
    return '\\' + char.charCodeAt(0).toString(16) + ' ';
  });
}

// Seguro: la codificación CSS previene inyección
const userColor = "red; background: url(javascript:alert('XSS'))";
element.style.cssText = `color: ${encodeCSS(userColor)}`;

Codificación de Contexto URL

Las URLs que contienen datos de usuario necesitan codificación por porcentaje:

// Usar codificación integrada para parámetros URL
const userSearch = "<script>alert('XSS')</script>";
const safeURL = `/search?q=${encodeURIComponent(userSearch)}`;

Manejando Contenido Enriquecido de Forma Segura

Muchas aplicaciones necesitan aceptar contenido HTML enriquecido de usuarios—publicaciones de blog, descripciones de productos o comentarios formateados. La codificación simple rompería el formato, por lo que necesitas sanitización HTML.

Usando DOMPurify para Sanitización HTML

DOMPurify proporciona sanitización HTML robusta que remueve elementos peligrosos mientras preserva el formato seguro:

import DOMPurify from 'dompurify';

// Configurar DOMPurify para tus necesidades
const clean = DOMPurify.sanitize(userHTML, {
  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
  ALLOWED_ATTR: ['href', 'title'],
  ALLOW_DATA_ATTR: false
});

// Seguro insertar HTML sanitizado
element.innerHTML = clean;

Patrones Seguros Específicos del Framework

Cada framework tiene patrones preferidos para manejar contenido generado por usuarios de forma segura:

React:

import DOMPurify from 'dompurify';

function Comment({ userContent }) {
  const sanitized = DOMPurify.sanitize(userContent);
  return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}

Vue.js:

<template>
  <div v-html="sanitizedContent"></div>
</template>

<script>
import DOMPurify from 'dompurify';

export default {
  computed: {
    sanitizedContent() {
      return DOMPurify.sanitize(this.userContent);
    }
  }
}
</script>

Angular:

import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import DOMPurify from 'dompurify';

export class CommentComponent {
  constructor(private sanitizer: DomSanitizer) {}
  
  getSafeContent(content: string): SafeHtml {
    const clean = DOMPurify.sanitize(content);
    return this.sanitizer.bypassSecurityTrustHtml(clean);
  }
}

Controles de Defensa en Profundidad

Mientras que la codificación y sanitización adecuadas proporcionan protección primaria, controles adicionales añaden capas de seguridad:

Content Security Policy (CSP)

Los headers CSP restringen qué scripts pueden ejecutarse, proporcionando una red de seguridad contra XSS:

// Ejemplo de Express.js
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'nonce-" + generateNonce() + "'"
  );
  next();
});

Atributos Seguros de Cookies

Establece las banderas HttpOnly y Secure en las cookies para limitar el impacto de XSS:

res.cookie('session', sessionId, {
  httpOnly: true,  // Previene acceso JavaScript
  secure: true,    // Solo HTTPS
  sameSite: 'strict'
});

Pruebas y Validación

Implementa pruebas automatizadas para detectar vulnerabilidades XSS:

// Ejemplo de prueba Jest
describe('XSS Prevention', () => {
  test('should encode HTML in comments', () => {
    const malicious = '<script>alert("XSS")</script>';
    const result = renderComment(malicious);
    expect(result).not.toContain('<script>');
    expect(result).toContain('&lt;script&gt;');
  });
});

Conclusión

Prevenir XSS en contenido generado por usuarios requiere un enfoque multicapa. Comienza con validación de entrada por lista de permitidos y normalización, aplica codificación de salida consciente del contexto basada en dónde se mostrarán los datos, y usa librerías probadas como DOMPurify para sanitización de contenido enriquecido. Mientras que los frameworks modernos proporcionan excelentes protecciones predeterminadas, entender cuándo y cómo usar de forma segura sus mecanismos de escape sigue siendo crítico. Recuerda que el filtrado por lista de denegados por sí solo nunca proporcionará protección adecuada—enfócate en definir qué está permitido en lugar de intentar bloquear cada patrón de ataque posible.

Preguntas Frecuentes

Usa una librería de sanitización HTML bien mantenida como DOMPurify. Configúrala para permitir solo etiquetas seguras como b, i, em, strong, a y p mientras eliminas etiquetas script, manejadores de eventos y atributos peligrosos. Siempre sanitiza del lado del servidor así como del lado del cliente para defensa en profundidad.

Almacena la entrada de usuario en su forma original en la base de datos y codifícala en el punto de salida. Este enfoque preserva los datos originales, te permite cambiar estrategias de codificación más tarde, y asegura que apliques la codificación correcta para cada contexto de salida.

Escapar convierte todas las etiquetas HTML a sus equivalentes de entidad, mostrándolas como texto en lugar de ejecutarlas. Sanitizar remueve elementos peligrosos mientras preserva el formato HTML seguro. Usa escape para campos de texto plano y sanitización para editores de contenido enriquecido.

Analiza markdown del lado del servidor usando una librería segura, luego sanitiza el HTML resultante con DOMPurify antes de enviarlo al cliente. Nunca confíes solo en el análisis markdown del lado del cliente, ya que los atacantes pueden evitarlo enviando HTML malicioso directamente a tu API.

Los frameworks modernos previenen XSS por defecto a través de escape automático, pero proporcionan mecanismos de escape como dangerouslySetInnerHTML que evitan estas protecciones. Debes asegurar manualmente la seguridad al usar estas características, al manejar archivos subidos por usuarios, o al construir dinámicamente URLs o valores CSS.

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers