Wiki/Vulnerabilidades/Cross-Site Request Forgery (CSRF)
Principiante
CVSS 6.5 - Medio
18 min lectura

Cross-Site Request Forgery (CSRF)

Fuerza a un usuario autenticado a ejecutar acciones no deseadas en una aplicación web

¿Qué es CSRF?

CSRF es un ataque que engaña al navegador de un usuario autenticado para que envíe una petición HTTP a una aplicación web en la que está autenticado, ejecutando una acción no autorizada.

Impacto

  • • Transferencias de dinero no autorizadas
  • • Cambio de email/contraseña de la víctima
  • • Creación/modificación/eliminación de datos
  • • Cambios en configuración de cuenta
  • • Acciones privilegiadas si la víctima es admin

Ejemplo de Ataque

// 1. Sitio bancario vulnerable
// POST /transfer
// Cookie: sessionId=valid_user_session

// 2. Atacante crea página maliciosa
<!-- evil-site.com/attack.html -->
<html>
  <body onload="document.forms[0].submit()">
    <form action="https://bank.com/transfer" method="POST">
      <input type="hidden" name="to" value="attacker_account">
      <input type="hidden" name="amount" value="10000">
    </form>
  </body>
</html>

// 3. Víctima visita evil-site.com
// El navegador envía automáticamente la cookie de sesión
// La transferencia se ejecuta sin que la víctima lo sepa

Vectores de Ataque

1. Formularios Auto-Submit

<form action="https://target.com/delete-account" method="POST">
  <input type="hidden" name="confirm" value="yes">
</form>
<script>document.forms[0].submit();</script>

2. IMG Tags con GET Requests

<!-- En email o sitio malicioso -->
<img src="https://bank.com/transfer?to=attacker&amount=1000" 
     style="display:none">

3. AJAX Cross-Origin (si CORS mal configurado)

fetch('https://target.com/api/change-email', {
  method: 'POST',
  credentials: 'include', // enviar cookies
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email: 'attacker@evil.com' })
});

Defensas contra CSRF

1. CSRF Tokens (Synchronizer Token)

Token único y aleatorio en cada formulario/petición.

// Backend - Generar token
import csrf from 'csurf';
const csrfProtection = csrf({ cookie: true });

app.get('/form', csrfProtection, (req, res) => {
  res.render('form', { csrfToken: req.csrfToken() });
});

// HTML - Incluir token en formulario
<form method="POST" action="/transfer">
  <input type="hidden" name="_csrf" value="<%= csrfToken %>">
  <input name="to" placeholder="Destinatario">
  <input name="amount" placeholder="Cantidad">
  <button>Transferir</button>
</form>

// Backend - Verificar token
app.post('/transfer', csrfProtection, (req, res) => {
  // csurf middleware verifica automáticamente
  // Si el token no coincide, rechaza con 403
  processTransfer(req.body);
});

// Next.js API Route
import { getCsrfToken } from 'next-auth/csrf';

export default async function handler(req, res) {
  const csrfToken = await getCsrfToken({ req });
  
  if (req.method === 'POST') {
    if (req.body.csrfToken !== csrfToken) {
      return res.status(403).json({ error: 'Invalid CSRF token' });
    }
    // Procesar petición
  }
}

2. SameSite Cookies

Prevenir envío de cookies en peticiones cross-site.

// Express.js
res.cookie('sessionId', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict' // o 'lax'
});

// SameSite=Strict: No envía cookie en ninguna petición cross-site
// SameSite=Lax: Envía cookie solo en navegación GET top-level
// SameSite=None: Requiere Secure (HTTPS)

// Next.js
export default function handler(req, res) {
  res.setHeader('Set-Cookie', 
    'session=abc123; HttpOnly; Secure; SameSite=Strict; Path=/'
  );
}

3. Double Submit Cookie

// 1. Servidor envía token en cookie
res.cookie('csrf-token', randomToken, { httpOnly: false });

// 2. Cliente lee cookie y envía en header
const csrfToken = getCookie('csrf-token');
fetch('/api/action', {
  method: 'POST',
  headers: {
    'X-CSRF-Token': csrfToken
  },
  body: JSON.stringify(data)
});

// 3. Servidor compara cookie vs header
app.post('/api/action', (req, res) => {
  const cookieToken = req.cookies['csrf-token'];
  const headerToken = req.headers['x-csrf-token'];
  
  if (cookieToken !== headerToken) {
    return res.status(403).json({ error: 'CSRF token mismatch' });
  }
  // Procesar
});

4. Custom Request Headers

AJAX requests con headers custom (navegador previene en cross-origin).

// Cliente
fetch('/api/action', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Requested-With': 'XMLHttpRequest' // header custom
  },
  body: JSON.stringify(data)
});

// Servidor verifica presencia del header
app.post('/api/action', (req, res) => {
  if (!req.headers['x-requested-with']) {
    return res.status(403).json({ error: 'Missing custom header' });
  }
  // Procesar
});

5. Verificar Origin/Referer Headers

app.post('/api/action', (req, res) => {
  const origin = req.headers.origin || req.headers.referer;
  const allowedOrigins = ['https://myapp.com'];
  
  if (!origin || !allowedOrigins.some(o => origin.startsWith(o))) {
    return res.status(403).json({ error: 'Invalid origin' });
  }
  // Procesar
});

Mejores Prácticas

  • Nunca usar GET para acciones que cambian estado - Solo POST, PUT, DELETE
  • Implementar CSRF tokens en todos los formularios - Especialmente acciones sensibles
  • Usar SameSite=Strict o Lax en cookies - Defensa en profundidad
  • Re-autenticar para acciones críticas - Transferencias, cambio de password
  • Implementar rate limiting - Limitar peticiones sospechosas

Recursos Adicionales

Siguiente Paso

Aprende sobre vulnerabilidades de control de acceso

Insecure Direct Object References (IDOR)