Proteger Formularios Web Contra Ataques CSRF
Cómo prevenir ataques de Cross-Site Request Forgery usando tokens de sincronización, cookies SameSite y patrones de double-submit cookie.
Visión general
Cross-Site Request Forgery (CSRF) engaña a usuarios autenticados para que realicen acciones no deseadas en un sitio web en el que confían. Un atacante crea un enlace o formulario malicioso que, cuando el usuario logueado hace clic, envía un request al sitio víctima usando la cookie de sesión existente del usuario. El servidor ve un request legítimo de un usuario autenticado y ejecuta la acción — cambiar un email, transferir fondos o eliminar una cuenta — sin conocimiento del usuario.
A diferencia de XSS, que inyecta scripts maliciosos, CSRF explota el comportamiento automático del navegador de enviar cookies. Si bank.com tiene un endpoint POST /transfer, un atacante puede incrustar un formulario en evil.com que se envía a bank.com/transfer. Mientras el usuario tenga una cookie de sesión válida para bank.com, el navegador la envía automáticamente.
Cuándo usarlo
Usa esta receta cuando:
- Construyes aplicaciones web con endpoints que cambian estado (POST, PUT, DELETE, PATCH)
- Implementas configuraciones de cuenta, flujos de pago o paneles administrativos
- Auditando aplicaciones existentes por vulnerabilidades CSRF
- Eligiendo entre synchronizer tokens, double-submit cookies y protección SameSite-only
Solución
Synchronizer Token Pattern (Django/Python)
from django.middleware.csrf import get_token
def render_form(request):
context = {'csrf_token': get_token(request)}
return render(request, 'form.html', context)
# Template
<form method="post" action="/settings/">
{% csrf_token %}
<input type="email" name="email" />
<button type="submit">Actualizar</button>
</form>
Double-Submit Cookie (Node.js/Express)
const crypto = require('crypto');
function generateCsrfToken(req, res) {
const token = crypto.randomBytes(32).toString('hex');
res.cookie('csrfToken', token, { httpOnly: false, sameSite: 'strict' });
return token;
}
function validateCsrfToken(req, res, next) {
const token = req.headers['x-csrf-token'] || req.body._csrf;
if (token !== req.cookies.csrfToken) {
return res.status(403).json({ error: 'Token CSRF inválido' });
}
next();
}
SameSite Cookie (Spring Boot)
@Configuration
public class CookieConfig implements WebMvcConfigurer {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setSameSite("Strict");
serializer.setUseSecureCookie(true);
return serializer;
}
}
Explicación
- Synchronizer tokens: Como
evil. comno puede leer el token del DOM o cookies debank. com, no puede forjar requests válidos. - Double-submit cookie: El servidor verifica que ambos valores coinciden. Es stateless — no requiere almacenamiento server-side — pero depende de que el atacante no pueda leer la cookie.
- SameSite cookies: Configurar
SameSite=StrictoLaxen cookies de sesión previene que el navegador las envíe con requests cross-origin. Es la defensa más simple y confiable, pero no todos los navegadores y escenarios la soportan perfectamente.
Variantes
| Técnica | Almacenamiento server | Stateless | Dependencia de navegador |
|---|---|---|---|
| Synchronizer token | Sí (sesión) | No | Ninguna |
| Double-submit cookie | No | Sí | Ninguna |
| SameSite cookie | No | Sí | Navegadores modernos |
| Custom headers | No | Sí | Solo AJAX |
Lo que funciona
- Usa SameSite=Strict en cookies de sesión: esto solo bloquea la mayoría de ataques CSRF. Combínalo con tokens para defensa en profundidad.
- Rota tokens CSRF por sesión, no por request: los tokens por-request rompen el botón de atrás y los workflows multi-tab. Los tokens por sesión son seguros y usables.
- Valida tokens para todos los métodos que cambian estado: verifica protección CSRF en POST, PUT, PATCH y DELETE. Los métodos seguros (GET, HEAD) no deberían cambiar estado de todos modos.
- Incluye tokens en headers de AJAX: para SPAs, lee el token desde un meta tag o cookie y envíalo como header custom (
X-CSRF-Token). - Rechaza tokens faltantes con 403: no ignores silenciosamente tokens faltantes. Un 403 señala una mala configuración o un intento de ataque.
Errores comunes
- Confiar solo en SameSite sin tokens: navegadores más antiguos y ciertos patrones de navegación cross-site pueden no enforce SameSite. Los tokens proveen una defensa de respaldo.
- No proteger formularios de login: login CSRF es real. Un atacante puede forzar a una víctima a loguearse en una cuenta controlada por el atacante, habilitando ataques subsecuentes.
- Usar GET para acciones que cambian estado:
GET /delete-account? id=123es trivialmente explotable vía una image tag o enlace. - Almacenar tokens en localStorage: XSS puede robar localStorage.
Soluciones Avanzadas
Double-submit cookie firmada (stateless, resistente a XSS)
El double-submit cookie básico es vulnerable si un atacante puede setear una cookie en el navegador de la víctima (ej. vía un subdominio). Firmar la cookie con un secreto server-side previene esto:
const crypto = require('crypto');
const CSRF_SECRET = process.env.CSRF_SECRET || 'rotate-this-secret';
function generateSignedCsrfToken(req, res) {
const token = crypto.randomBytes(32).toString('base64url');
const signature = crypto
.createHmac('sha256', CSRF_SECRET)
.update(token)
.digest('base64url');
const signedToken = `${token}.${signature}`;
res.cookie('csrfToken', signedToken, {
httpOnly: false,
sameSite: 'strict',
secure: true,
path: '/',
});
return signedToken;
}
function validateSignedCsrfToken(req, res, next) {
const headerToken = req.headers['x-csrf-token'];
const cookieToken = req.cookies.csrfToken;
if (!headerToken || !cookieToken) {
return res.status(403).json({ error: 'Falta token CSRF' });
}
if (headerToken !== cookieToken) {
return res.status(403).json({ error: 'Token CSRF no coincide' });
}
// Verificar firma
const [token, signature] = cookieToken.split('.');
if (!token || !signature) {
return res.status(403).json({ error: 'Formato de token CSRF inválido' });
}
const expectedSig = crypto
.createHmac('sha256', CSRF_SECRET)
.update(token)
.digest('base64url');
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSig)
)) {
return res.status(403).json({ error: 'Firma de token CSRF inválida' });
}
next();
}
// Setup Express
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
app.use(cookieParser());
// Generar token en GET (renderizar página de formulario)
app.get('/form', (req, res) => {
const token = generateSignedCsrfToken(req, res);
res.json({ csrfToken: token });
});
// Validar token en POST (enviar formulario)
app.post('/submit', validateSignedCsrfToken, (req, res) => {
res.json({ success: true });
});
Protección CSRF para SPAs (React + interceptor fetch)
Las single-page applications necesitan tokens CSRF accesibles desde JavaScript. Usa una cookie non-HttpOnly y un meta tag o endpoint de API:
// Utilidad CSRF en React — obtener token de API al iniciar la app
let csrfToken = null;
export async function initCsrf() {
const res = await fetch('/api/csrf-token', {
credentials: 'same-origin',
});
const data = await res.json();
csrfToken = data.token;
return csrfToken;
}
// Interceptor fetch: adjuntar token a todos los requests que cambian estado
export function csrfFetch(url, options = {}) {
const method = (options.method || 'GET').toUpperCase();
// Solo adjuntar token CSRF a métodos que cambian estado
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
options.headers = {
...options.headers,
'X-CSRF-Token': csrfToken,
};
}
return fetch(url, {
...options,
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
}
// Uso en un componente React
import { useState, useEffect } from 'react';
function SettingsForm() {
const [email, setEmail] = useState('');
useEffect(() => {
initCsrf();
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
const res = await csrfFetch('/api/settings', {
method: 'POST',
body: JSON.stringify({ email }),
});
if (res.ok) {
alert('Configuración actualizada');
} else {
alert('Error al actualizar configuración');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">Actualizar</button>
</form>
);
}
Validación de headers Origin y Referer
Como capa adicional, valida que el header Origin o Referer coincida con tu sitio:
from django.http import HttpResponseForbidden
from urllib.parse import urlparse
ALLOWED_ORIGINS = {'https://myapp.com', 'https://www.myapp.com'}
def validate_origin(get_response):
"""Middleware que valida Origin/Referer en requests que cambian estado."""
def middleware(request):
if request.method in ('POST', 'PUT', 'PATCH', 'DELETE'):
origin = request.headers.get('Origin')
referer = request.headers.get('Referer')
source = origin or referer
if not source:
return HttpResponseForbidden('Falta header Origin')
parsed = urlparse(source)
if f"{parsed.scheme}://{parsed.netloc}" not in ALLOWED_ORIGINS:
return HttpResponseForbidden('Origin inválido')
return get_response(request)
return middleware
Rotación de token por request con almacenamiento en sesión
Para aplicaciones de alta seguridad, rota tokens por request en lugar de por sesión:
import secrets
from flask import Flask, session, request, jsonify, abort
app = Flask(__name__)
app.secret_key = 'rotate-this-secret'
@app.before_request
def generate_csrf_token():
if request.method == 'GET':
session['csrf_token'] = secrets.token_urlsafe(32)
@app.route('/form')
def form_page():
token = session.get('csrf_token')
return jsonify({'csrf_token': token})
@app.route('/submit', methods=['POST'])
def submit():
token = request.headers.get('X-CSRF-Token', '')
session_token = session.get('csrf_token', '')
if not token or not session_token:
abort(403, description='Falta token CSRF')
if not secrets.compare_digest(token, session_token):
abort(403, description='Token CSRF inválido')
# Rotar token para el próximo request
session['csrf_token'] = secrets.token_urlsafe(32)
return jsonify({'success': True, 'next_token': session['csrf_token']}) Preguntas frecuentes
¿Esta solución está lista para producción?
Sí. Los ejemplos de código arriba muestran implementaciones probadas. Adapta el manejo de errores y la configuración a tu entorno específico antes de desplegar.
¿Cuáles son las características de rendimiento?
El rendimiento depende de tu volumen de datos e infraestructura. Las soluciones mostradas priorizan claridad. Para escenarios de alto throughput, añade caching, batching y connection pooling según sea necesario.
¿Cómo depuro problemas con este enfoque?
Empieza con el ejemplo mínimo de arriba. Añade logging en cada paso. Prueba con entradas pequeñas primero, luego escala. Usa el debugger de tu lenguaje para revisar los edge cases.
Recursos Relacionados
Asegurar APIs con HTTP Security Headers
Cómo configurar headers de seguridad esenciales como HSTS, CSP y X-Frame-Options para proteger APIs y aplicaciones web de ataques comunes.
RecipeImplementar Gestión de Sesiones Segura
Cómo crear, validar y expirar sesiones de usuario de forma segura en aplicaciones web usando cookies, tokens y almacenamiento server-side.
RecipePrevenir Cross-Site Scripting (XSS)
Cómo sanitizar input de usuario, escapar output y usar Content Security Policy para prevenir ataques XSS en aplicaciones web.
RecipeHashing de Contraseñas en Producción
Hashea y verifica contraseñas de forma segura usando bcrypt, scrypt y Argon2 con lo que funciona.
RecipeEncripción en Reposo para Bases de Datos y Almacenamiento
Cómo encriptar datos sensibles antes de almacenarlos en bases de datos, object storage y backups usando AES-256-GCM, encripción de sobre y servicios de gestión de keys.
RecipeFirma de Requests con HMAC
Asegura requests de APIs con firmas HMAC-SHA256 para garantizar integridad y autenticidad.