Saltar al contenido principal

Cross-Site Request Forgery (CSRF)

Cross-Site Request Forgery (CSRF) es un ataque que engaña al navegador de un usuario autenticado para que realice una petición no deseada a una aplicación web en la que tiene sesión activa, sin que el usuario sea consciente de ello.

El ataque explota el hecho de que los navegadores envían automáticamente las cookies de sesión en cada petición al dominio correspondiente, independientemente de desde dónde se origine esa petición.

CSRF no roba datos directamente: ejecuta acciones en nombre de la víctima.

¿Qué puede hacer un atacante con CSRF?

  • Cambiar la contraseña o el correo de la víctima.
  • Realizar transferencias bancarias.
  • Publicar contenido en nombre del usuario.
  • Borrar datos o modificar la configuración de la cuenta.

Cómo funciona el ataque

Condiciones necesarias:

  1. La víctima debe estar autenticada en la aplicación objetivo.
  2. La aplicación debe confiar únicamente en la cookie de sesión para verificar las peticiones.
  3. El atacante debe poder predecir los parámetros de la petición.

Imaginemos un banco con este endpoint:

POST /transferir
Cookie: session=abc123

{
"destinatario": "ES12345678",
"cantidad": 1000
}

El atacante crea una página web maliciosa con este formulario oculto:

<!-- En el sitio del atacante: evil.com -->
<form action="https://mibanco.com/transferir" method="POST" id="formularioOculto">
<input type="hidden" name="destinatario" value="ES00ATACANTE">
<input type="hidden" name="cantidad" value="5000">
</form>

<script>
document.getElementById('formularioOculto').submit();
</script>

Cuando la víctima visita evil.com mientras tiene sesión en mibanco.com:

  1. El navegador carga evil.com.
  2. El script envía automáticamente el formulario a mibanco.com.
  3. El navegador adjunta la cookie de sesión válida de la víctima.
  4. El banco procesa la transferencia como legítima (desde su perspectiva).

Variantes del ataque

Con una etiqueta <img>

Para peticiones GET, basta con insertar una imagen con la URL del endpoint:

<img src="https://mibanco.com/transferir?destinatario=ES00ATACANTE&cantidad=5000">
Peticions GET y acciones

Por este motivo, las acciones con efecto secundario (crear, modificar, borrar) nunca deben implementarse con peticiones GET.

Con fetch desde otro origen

// Código en evil.com
fetch('https://mibanco.com/transferir', {
method: 'POST',
credentials: 'include', // Envía las cookies de sesión
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ destinatario: 'ES00ATACANTE', cantidad: 5000 })
});

Cómo prevenir CSRF

Tokens CSRF

La defensa más clásica es utilizar STP (Synchronizer Token Pattern). El servidor genera un token aleatorio e impredecible asociado a la sesión del usuario. Este token debe incluirse en cada formulario o petición que modifique estado. El servidor verifica que el token sea correcto antes de procesar la petición.

Un atacante no puede conocer ese token porque no tiene acceso a la sesión ni a las respuestas del servidor origen.

Ejemplo en Express:

npm install cookie-parser csrf-csrf
import { doubleCsrf } from "csrf-csrf";

const { generateToken, doubleCsrfProtection } = doubleCsrf({
getSecret: () => process.env.CSRF_SECRET,
cookieName: 'csrf-token',
cookieOptions: { sameSite: 'strict', secure: true },
});

// Ruta para obtener el token (se envía al frontend)
app.get('/csrf-token', (req, res) => {
res.json({ token: generateToken(req, res) });
});

// Aplicar protección a rutas que modifican estado
app.use('/api', doubleCsrfProtection);

app.post('/transferir', (req, res) => {
// Si llegamos aquí, el token CSRF era válido
res.json({ ok: true });
});

El frontend debe enviar el token en cada petición:

// Obtener el token al cargar la aplicación
const { token } = await fetch('/csrf-token').then(r => r.json());

// Incluirlo en cada petición que modifique estado
await fetch('/transferir', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token // Token en cabecera
},
body: JSON.stringify({ destinatario: 'ES12345678', cantidad: 100 })
});

Atributo SameSite

Configura las cookies de sesión con el atributo SameSite para que el navegador no las envíe en peticiones entre sitios:

res.cookie('sessionId', token, {
httpOnly: true,
secure: true,
sameSite: 'Strict' // Las cookies no se envían en peticiones cross-site
// O también: sameSite: 'Lax' (más permisivo, pero cubre la mayoría de casos)
});

El atributo puede tomar los siguientes valores:

  • Strict: La cookie solo se envía en peticiones del mismo sitio.
  • Lax: Se envía en navegación de nivel superior (clics en enlaces), pero no en peticiones de recursos cross-site.
  • None: Siempre se envía (requiere Secure).
Valor por defecto

SameSite: Lax es ahora el valor por defecto en los navegadores modernos, lo que mitiga muchos ataques CSRF automáticamente.

Verificar el Header Origin o Referer

El servidor puede comprobar de dónde viene la petición:

app.use((req, res, next) => {
const origin = req.headers.origin || req.headers.referer;

if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) {
if (!origin || !origin.startsWith('https://miaplicacion.com')) {
return res.status(403).json({ error: 'Origen no permitido' });
}
}
next();
});

Esta medida puede fallar si el navegador omite la cabecera Referer (configuraciones de privacidad). Debe usarse como capa adicional, no como única defensa.

CORS bien configurado

Asegúrate de que CORS solo permita orígenes de confianza:

const cors = require('cors');

app.use(cors({
origin: 'https://miaplicacion.com', // Solo este origen
credentials: true
}));

CSRF vs XSS

Es habitual confundirlos. Aquí la diferencia clave:

XSSCSRF
VectorInyectar código en la páginaEngañar al navegador para que envíe una petición
ObjetivoEjecutar JS en el navegador de la víctimaRealizar acciones en nombre de la víctima
Requiere sesión activaNo necesariamente
Puede saltarse CSRF tokensSí (si hay XSS, puede leer el token)No aplica
Importante

Si una aplicación tiene una vulnerabilidad XSS, la protección CSRF puede quedar anulada, ya que el atacante podría leer el token CSRF desde el DOM.