El CSRF (Cross-Site Request Forgery) es una de las tres vulnerabilidades web clásicas, junto al SQL Injection y el XSS. Y tiene una particularidad: no necesita robar ninguna contraseña. Le basta con que la víctima ya tenga una sesión abierta en tu aplicación.
Cómo funciona el ataque
Cuando un usuario inicia sesión, 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. Un atacante puede aprovecharlo con una página (o un email, o un anuncio) que lance una petición a tu sitio. Si tu aplicación solo comprueba que hay sesión, la ejecutará como si la hubiera hecho el propio usuario.
Imagina un formulario para cambiar el email de la cuenta que solo comprueba $_SESSION['user_id']. El atacante monta una página externa con un formulario oculto que apunta a tu endpoint y se envía solo con JavaScript. Si la víctima la visita estando logueada, su email cambia sin que se entere. Desde ahí, al atacante le basta con pedir un «olvidé mi contraseña» para quedarse con la cuenta.
La defensa principal: un token CSRF
La defensa estándar, recomendada por OWASP, es un token aleatorio generado por el servidor, que viaja en cada formulario y se valida al recibir la petición. El atacante no puede conocerlo ni adivinarlo, así que no puede incluirlo en su formulario falso.
Genera el token y guárdalo en sesión:
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
Inclúyelo en el formulario:
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
Y valídalo antes de procesar nada:
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.');
}
Dos detalles importantes: usa random_bytes() (nunca rand() ni uniqid()) y compara con hash_equals() en lugar de ==, para evitar timing attacks. En acciones muy sensibles, como cambiar la contraseña, invalida el token después de usarlo con unset($_SESSION['csrf_token']).
Capas adicionales
El token es la defensa principal, pero conviene combinarlo con:
-
Cookies SameSite: con
SameSite=LaxoStrict, el navegador no envía la cookie de sesión en peticiones que vienen de otros dominios. -
Comprobar
OriginoReferer: solo como capa extra, nunca como única defensa, porque pueden faltar en casos legítimos. -
Usar POST, nunca GET, para acciones que modifican datos: un enlace GET se explota con un simple
<img src="...">. -
HTTPS en todo el sitio, para que las cookies
securese envíen siempre.
Si usas un framework como Laravel o Symfony, ya trae protección CSRF integrada: actívala y no la desactives «para simplificar».
Conclusión
Con un token aleatorio, hash_equals() y cookies SameSite cubres la gran mayoría de los casos reales de CSRF en PHP, sin librerías complicadas.
Si quieres el ejemplo completo y la explicación detallada, está en el artículo original en Todo se Aprende.
Top comments (2)
Yes indeed. CSRF Tokens are definitely a must have. They're way more reliable than SameSite=Lax, but still, developers should always use both to be on the secure side.
Totally agree, that's the idea: the token as the main defense and SameSite as an extra layer. Thanks for reading!