DEV Community

Marius Podar
Marius Podar

Posted on Originally published at todoseaprende.com

CSRF en PHP: qué es y cómo prevenirlo con un token (guía rápida)

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));
}
Enter fullscreen mode Exit fullscreen mode

Inclúyelo en el formulario:

<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
Enter fullscreen mode Exit fullscreen mode

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.');
}
Enter fullscreen mode Exit fullscreen mode

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=Lax o Strict, el navegador no envía la cookie de sesión en peticiones que vienen de otros dominios.
  • Comprobar Origin o Referer: 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 secure se 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)

Collapse
 
mattdrmmnd profile image
Matt Drummond •

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.

Collapse
 
marius_podar profile image
Marius Podar •

Totally agree, that's the idea: the token as the main defense and SameSite as an extra layer. Thanks for reading!