CSRF en PHP: qué es y cómo prevenirlo
La seguridad web es un aspecto fundamental en el desarrollo de aplicaciones modernas, y ya hemos hablado en el blog de dos de los ataques más comunes: el SQL Injection y el XSS. Hoy toca cerrar el trío de amenazas clásicas hablando de CSRF en PHP: qué es el Cross-Site Request Forgery, cómo engaña a un usuario ya autenticado para que, sin saberlo, ejecute una acción no deseada en tu aplicación, y sobre todo, cómo protegerte.
A diferencia del XSS (que inyecta código) o el SQLi (que manipula consultas), el CSRF no necesita robar ninguna credencial: le basta con aprovechar que el navegador de la víctima ya tiene una sesión activa y válida en tu web.
En este artículo veremos cómo funciona un ataque CSRF, un ejemplo práctico de formulario vulnerable, y cómo protegerlo en PHP con tokens de seguridad, comparaciones seguras y cabeceras adicionales.
¿Qué es CSRF en PHP y cómo funciona un ataque?
Cuando un usuario ha iniciado sesión en tu web, el navegador guarda una cookie de sesión y la envía automáticamente en cada petición a ese dominio, sin importar desde qué página se origine la petición.
Un atacante puede aprovechar esto creando una página (o email, o anuncio) que, al visitarla, envíe automáticamente una petición a tu sitio. Si tu aplicación no verifica el origen de esa petición, la ejecutará como si la hubiera hecho el propio usuario.
Ejemplo de formulario vulnerable
Imagina un formulario para cambiar el email de la cuenta:
<?php
// cambiar-email.php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$nuevo_email = $_POST['email'];
$stmt = $pdo->prepare("UPDATE usuarios SET email = ? WHERE id = ?");
$stmt->execute([$nuevo_email, $_SESSION['user_id']]);
echo "Email actualizado";
}
?>
<form action="cambiar-email.php" method="POST">
<input type="email" name="email">
<button type="submit">Guardar</button>
</form>
Lenguaje del código: HTML, XML (xml)Este código no verifica de dónde viene la petición, solo que el usuario tenga sesión iniciada.
Ataque de ejemplo
El atacante monta una página externa con un formulario que apunta a tu endpoint y la envía automáticamente con JavaScript:
<body onload="document.forms[0].submit()">
<form action="https://tuweb.com/cambiar-email.php" method="POST">
<input type="hidden" name="email" value="atacante@evil.com">
</form>
</body>
Lenguaje del código: HTML, XML (xml)Si la víctima (con sesión activa en tuweb.com) visita esta página, el navegador envía la cookie de sesión automáticamente y el email de su cuenta se cambia sin que se dé cuenta. A partir de ahí, el atacante puede pedir un «olvidé mi contraseña» y tomar control de la cuenta.

¿Cómo prevenir el CSRF en PHP?
La defensa estándar, recomendada también por OWASP en su CSRF Prevention Cheat Sheet, es el token CSRF: un valor aleatorio único, generado por el servidor, que se incluye en cada formulario y se valida al recibir la petición. Al ser un dato que el atacante no puede conocer ni adivinar, no puede incluirlo en su formulario falso.
1. Generar y guardar el token en sesión
<?php
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
Lenguaje del código: HTML, XML (xml)random_bytes() genera bytes criptográficamente seguros, y bin2hex() los convierte en una cadena hexadecimal fácil de insertar en HTML.
2. Incluir el token en el formulario
<form action="cambiar-email.php" method="POST">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="email" name="email">
<button type="submit">Guardar</button>
</form>
Lenguaje del código: HTML, XML (xml)3. Validar el token al recibir la petición
<?php
// cambiar-email.php
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (
empty($_POST['csrf_token']) ||
empty($_SESSION['csrf_token']) ||
!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])
) {
http_response_code(403);
die("Petición no válida.");
}
$nuevo_email = $_POST['email'];
$stmt = $pdo->prepare("UPDATE usuarios SET email = ? WHERE id = ?");
$stmt->execute([$nuevo_email, $_SESSION['user_id']]);
echo "Email actualizado";
}
?>
Lenguaje del código: HTML, XML (xml)Es importante usar hash_equals() en lugar de == o === para comparar los tokens: evita los llamados timing attacks, donde un atacante podría deducir el token midiendo cuánto tarda el servidor en rechazar cada intento.
4. Regenerar el token tras usarlo (opcional pero recomendable)
Para acciones especialmente sensibles (cambio de contraseña, transferencias, etc.), es buena práctica invalidar el token después de usarlo una vez, para que no se pueda reutilizar:
unset($_SESSION['csrf_token']);
Lenguaje del código: PHP (php)Capas adicionales de protección
El token CSRF es la defensa principal, pero conviene combinarla con:
- Cookies
SameSite: configurando la cookie de sesión conSameSite=LaxoSameSite=Strict, el navegador deja de enviarla en peticiones que provienen de otros dominios.session_set_cookie_params([ 'samesite' => 'Strict', 'secure' => true, 'httponly' => true ]); session_start(); - Verificar el header
OriginoReferer: como comprobación extra (nunca como única defensa, ya que pueden faltar en algunos escenarios legítimos):$origen = $_SERVER['HTTP_ORIGIN'] ?? $_SERVER['HTTP_REFERER'] ?? ''; if (!str_starts_with($origen, 'https://tuweb.com')) { http_response_code(403); die("Origen no permitido."); } - Usar siempre POST (nunca GET) para acciones que modifican datos: los enlaces GET son mucho más fáciles de explotar (basta un
<img src="...">para disparar la petición).
Buenas prácticas
- Genera el token con
random_bytes(), nunca conrand()ouniqid(). - Compara tokens siempre con
hash_equals(). - Combina el token CSRF con cookies
SameSiteyHttpOnly. - Usa HTTPS en todo el sitio (una cookie
secureno se envía por HTTP). - No confíes solo en el
Referer: úsalo como capa extra, no como defensa principal. - Si usas un framework (Laravel, Symfony, etc.), ya trae protección CSRF integrada — actívala y no la desactives «para simplificar».
Conclusión
Proteger tu aplicación de CSRF en PHP no requiere librerías complicadas: con un token aleatorio, hash_equals() para compararlo y cookies SameSite, cubres la gran mayoría de los casos reales. Combínalo con lo que ya vimos sobre SQL Injection y XSS y tendrás cubiertas las tres vulnerabilidades más explotadas en aplicaciones PHP.