TechLead
Lección 3 de 9
5 min de lectura
Seguridad Web

Protección CSRF

Protege tus aplicaciones de ataques Cross-Site Request Forgery con tokens, cookies SameSite y validación adecuada.

Entendiendo los Ataques CSRF

Cross-Site Request Forgery (CSRF) engaña a los usuarios para que realicen acciones no deseadas en un sitio donde están autenticados. El ataque explota la confianza que un sitio tiene en el navegador del usuario.

Cómo Funciona CSRF

<!-- Página maliciosa del atacante -->
<html>
<body>
  <h1>¡Ganaste un premio!</h1>

  <!-- Formulario oculto que se envía automáticamente -->
  <form action="https://banco.com/transferir" method="POST" id="evil-form">
    <input type="hidden" name="to" value="cuenta-atacante">
    <input type="hidden" name="amount" value="10000">
  </form>

  <script>
    document.getElementById('evil-form').submit();
  </script>
</body>
</html>

<!-- Si el usuario está logueado en banco.com, ¡esta transferencia se ejecutará! -->

Métodos de Protección CSRF

1. Tokens CSRF (Patrón de Token Sincronizador)

// Lado del servidor: Generar token
const crypto = require('crypto');

function generateCSRFToken(session) {
  const token = crypto.randomBytes(32).toString('hex');
  session.csrfToken = token;
  return token;
}

// Incluir token en formularios
app.get('/transferir', (req, res) => {
  const token = generateCSRFToken(req.session);
  res.render('transferir', { csrfToken: token });
});

// Validar token en el envío
app.post('/transferir', (req, res) => {
  const { csrfToken } = req.body;

  if (!csrfToken || csrfToken !== req.session.csrfToken) {
    return res.status(403).json({ error: 'Token CSRF inválido' });
  }

  // Procesar la transferencia
});
<!-- Incluir token en formulario -->
<form action="/transferir" method="POST">
  <input type="hidden" name="csrfToken" value="{{csrfToken}}">
  <input type="text" name="to" placeholder="Destinatario">
  <input type="number" name="amount" placeholder="Cantidad">
  <button type="submit">Transferir</button>
</form>

2. Cookies SameSite

// Establecer atributo SameSite en cookies
res.cookie('sessionId', sessionId, {
  httpOnly: true,      // No accesible vía JavaScript
  secure: true,        // Solo enviada sobre HTTPS
  sameSite: 'strict',  // No enviada en solicitudes cross-site
  maxAge: 3600000,     // 1 hora
});

// Opciones de SameSite:
// 'strict' - Cookie nunca se envía en solicitudes cross-site
// 'lax'    - Cookie se envía en navegaciones de nivel superior (por defecto en navegadores modernos)
// 'none'   - Cookie siempre se envía (requiere flag Secure)

3. Patrón de Doble Envío de Cookie

// Establecer token CSRF en cookie y requerirlo en header
app.use((req, res, next) => {
  if (!req.cookies.csrfToken) {
    const token = crypto.randomBytes(32).toString('hex');
    res.cookie('csrfToken', token, { sameSite: 'strict' });
  }
  next();
});

// Cliente envía token en header
fetch('/api/transferir', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': getCookie('csrfToken'),
  },
  body: JSON.stringify(data),
});

// Servidor valida
app.post('/api/transferir', (req, res) => {
  const cookieToken = req.cookies.csrfToken;
  const headerToken = req.headers['x-csrf-token'];

  if (!cookieToken || cookieToken !== headerToken) {
    return res.status(403).json({ error: 'Validación CSRF falló' });
  }

  // Procesar solicitud
});

4. Usando Middleware csurf (Express)

const csrf = require('csurf');
const cookieParser = require('cookie-parser');

app.use(cookieParser());
app.use(csrf({ cookie: true }));

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

// Validación ocurre automáticamente en POST/PUT/DELETE

5. Protección CSRF en Next.js

// lib/csrf.ts
import Tokens from 'csrf';

const tokens = new Tokens();
const secret = process.env.CSRF_SECRET!;

export function generateToken(): string {
  return tokens.create(secret);
}

export function verifyToken(token: string): boolean {
  return tokens.verify(secret, token);
}

// Ruta API
import { verifyToken } from '@/lib/csrf';

export async function POST(request: Request) {
  const token = request.headers.get('x-csrf-token');

  if (!token || !verifyToken(token)) {
    return Response.json({ error: 'Token CSRF inválido' }, { status: 403 });
  }

  // Procesar solicitud
}

// Componente React
function Form({ csrfToken }) {
  const handleSubmit = async (data) => {
    await fetch('/api/submit', {
      method: 'POST',
      headers: {
        'X-CSRF-Token': csrfToken,
      },
      body: JSON.stringify(data),
    });
  };

  return <form onSubmit={handleSubmit}>...</form>;
}

Mitigaciones Adicionales de CSRF

// Verificar headers Origin/Referer
function validateOrigin(req) {
  const origin = req.headers.origin || req.headers.referer;
  const allowedOrigins = ['https://misitio.com'];

  if (!origin) return false;

  try {
    const url = new URL(origin);
    return allowedOrigins.includes(url.origin);
  } catch {
    return false;
  }
}

// Requerir re-autenticación para acciones sensibles
app.post('/cambiar-password', requireReauth, (req, res) => {
  // Solo proceder si el usuario se re-autenticó recientemente
});

// Usar headers personalizados (no se pueden establecer cross-origin sin CORS)
fetch('/api/data', {
  headers: {
    'X-Requested-With': 'XMLHttpRequest',
  },
});

Lista de Verificación CSRF

  • Usa tokens CSRF para todas las operaciones que cambian estado
  • Establece SameSite=Strict o Lax en cookies de sesión
  • Valida headers Origin/Referer como verificación adicional
  • Usa POST para operaciones que cambian estado (nunca GET)
  • Requiere re-autenticación para acciones sensibles
  • Considera usar headers de solicitud personalizados

Continuar Aprendiendo