Cómo prevenir ataques CSRF con PHP

PHP

En este tutorial vamos a ver cómo puedes evitar ataques CSRF en tus aplicaciones PHP. Estos ataques suelen provocar que las cuentas de los usuarios puedan ser comprometidas y que sus datos privados se filtren al público, entre otras cosas.

En qué consiste un ataque CSRF

Los ataques CSRF (Cross Site Request Forgery) o XSRF ocurren cuando un atacante inyecta código HTML no deseado en la aplicación de algún servidor A. Entonces, el usuario accede legítimamente a otro servidor B. A su vez, el usuario accede al servidor A mediante un enlace del servidor B, obteniendo el código HTML malicioso, que realizará una petición al servidor B sin que el usuario lo sepa, pudiendo obtener información privada del mismo.

Cómo evitar ataques CSRF con PHP

Existen diversos métodos mediante los cuales podemos evitar ataques CSRF usando PHP. El más sencillo consiste en almacenar un token genérico de seguridad en una variable de sesión en el servidor.

Cómo generar el token CSRF

Primero veremos cómo generar el token. Usaremos el mejor método disponible según tu versión de PHP y las extensiones que tengas instaladas.

Si usas PHP 7 o PHP 8, usa este código para iniciar sesión y generar un token de seguridad mediante la función bin2hex, a la que pasaremos una cadena arbitraria de 32 bytes creada con la función random_bytes:

session_start();

if (empty($_SESSION['token'])) {
  $_SESSION['token'] = bin2hex(random_bytes(32));
}
$token = $_SESSION['token'];

Información acerca de las funciones utilizadas:

  • Para más información acerca de la función bin2hex consulta este enlace.
  • Para más información acerca de la función random_bytes consulta este enlace.

Si usas PHP 5 tendrás que usar otro método que funcionará con PHP 5 siempre y cuando sea la versión 5.3 o superior, o también en caso de disponer de la extensión mcrypt. Usaremos la función bin2hex, a la que pasaremos 32 bytes aleatorios que pueden estar generados de dos formas.

Si mcrypt está presente, generamos los bytes mediante la función mcrypt_create_iv. De lo contrario, usamos la función openssl_random_pseudo_bytes:

session_start();

if (empty($_SESSION['token'])) {
  if (function_exists('mcrypt_create_iv')) {
    $_SESSION['token'] = bin2hex(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM));
  } else {
    $_SESSION['token'] = bin2hex(openssl_random_pseudo_bytes(32));
  }
}
$token = $_SESSION['token'];

Información acerca de las funciones utilizadas:

  • Para más información acerca de la función mcrypt_create_iv consulta este enlace.
  • Para más información acerca de la función openssl_random_pseudo_bytes consulta este enlace.

A continuación veremos cómo mostrar el token en un formulario.

Cómo agregar el token CSRF

Una vez generado el token, debemos agregarlo a los formularios HTML. Vamos a mostrar un campo input oculto y para ello usaremos el atributo hidden:

<input name="csrf" type="hidden" value="<?php echo $_SESSION['csrf']; ?>">

Cómo validar el token CSRF

Finalmente, tendremos que validar el token cuando se envíe el formulario. Para ello usaremos la función hash_equals, disponible desde PHP 5.6:

if (!empty($_POST['token'])) {
  if (hash_equals($_SESSION['token'], $_POST['token'])) {
    // Procesar el formulario
  } else {
    // Posible petición malintencionada
    // Se recomienda guardar este acceso en un log
  }
}

Para más información acerca de esta función hash_equals, consulta este enlace. En ningún caso se recomienda usar los operadores de comparación == o === para verificar los tokens.

En ningún caso debes usar las funciones rand, uniqid o md5 para generar los tokens, ya que tanto rand como md5 son predecibles y deterministas. En cuando a uniqid, solamente agrega 29 bits de entropía.

Protección CSRF avanzada

Puedes restringir el uso de un token a un único formulario mediante el uso de la función hash_hmac. La función HMAC es una función usada para generar hashes que podemos usar, por ejemplo, con la función sha256.

Primero debemos generar un segundo token según los métodos que ya hemos visto.

Luego, muestra el token usando este código en el formulario:

<input type="hidden" name="token" value="<?= hash_hmac('sha256', '/formulario.php', $_SESSION['token_secundario']); ?>" />

Para validar el token usa este código:

$calc = hash_hmac('sha256', '/formulario.php', $_SESSION['token_secundario']);
if (hash_equals($calc, $_POST['token'])) {
  // Procesar el formulario
}

Será imposible usar los tokens generador para un formulario en otro formulario, ya que sería necesario conocer el valor del token $_SESSION['second_token'].

También podrías generar tokens de un único uso, aunque esto invalidaría los tokens que se estén mostrando en pestañas secundarias del navegador del usuario. El método más simple de generar tokens de uso único consiste en regenerar el token cada vez que este se valide.

Una solución que ayuda con el problema de las pestañas consiste en almacenar la lista de tokens en memoria hasta que sean usados o en usar esta librería anti CSRF, que agregará tokens de un único uso por cada formulario existente.

Protección CSRF con Twig

Si usas el motor de plantillas Twig puedes usar el siguiente filtro, que tendrás que agregar a tu entorno Twig mediante el método addFunction:

$twigEnv->addFunction(
  new \Twig_SimpleFunction(
    'form_token',
    function($lock_to = null)
    {
      if (empty($_SESSION['token'])) {
        $_SESSION['token'] = bin2hex(random_bytes(32));
      }
      
      if (empty($_SESSION['token2'])) {
        $_SESSION['token2'] = random_bytes(32);
      }
      
      if (empty($lock_to)) {
        return $_SESSION['token'];
      }
      return hash_hmac('sha256', $lock_to, $_SESSION['token2']);
    }
  )
);

Para mostrar un token genérico bastará con que agregues el campo input tal que así:

<input type="hidden" name="token" value="{{ form_token() }}" />

En caso de que quieras mostrar un token asociado a un único formulario, puedes mostrarlo de este modo:

<input type="hidden" name="token" value="{{ form_token('/formulario.php') }}" />

Protección CSRF con Laravel

Si usas el framework Laravel, has de saber que ya integra protección CSRF por defecto en todos los formularios. Lo único que debes hacer es agregar el campo input con el token:

<input type="hidden" name="_token" value="{{ csrf_token() }}" />

Si has creado una aplicación SPA que utiliza una API creada con Laravel y quieres agregar protección CSRF, consulta la documentación de Laravel Sanctum.

Y esto ha sido todo.

También puedes consultar cómo evitar ataques XSS con PHP.


Avatar de Edu Lazaro

Edu Lázaro: Ingeniero técnico en informática, actualmente trabajo como desarrollador web y programador de videojuegos.

👋 Hola! Soy Edu, me encanta crear cosas y he redactado esta guía. Si te ha resultado útil, el mayor favor que me podrías hacer es el de compatirla en Twitter 😊

Si quieres conocer mis proyectos, sígueme en Twitter.

2 comentarios en “Cómo prevenir ataques CSRF con PHP

  1. Hola Edu
    He llegado aquí, buceando en la búsqueda de información sobre token, me parece un buen tutorial, y bien explicado.
    Quería preguntarte por el token de Protección CSRF avanzada, por lo visto siempre es el mismo, es decir, no cambia nunca.
    1.- ¿Esto no supondría un problema de seguridad?
    2.- ¿Sería conveniente crear un token diferente para cada sesión de usuario?
    Gracias por tu tiempo.
    Un saludo

  2. Saludos Ingeniero, excelente tutorial. Estoy desarrollando un nuevo fremeword mvc en php 8x nativo y en verdad que la técnica Protección CSRF avanzada me ha sido de mucha utilidad, pues la pienso implementar por cada vista servida con un toque diferente y generando uno nuevo por cada petición que haga con las mismas vistas.

    Muchas gracias y mucha salud.

    Saludos

Deja una respuesta

“- Hey, Doc. No tenemos suficiente carretera para ir a 140/h km. - ¿Carretera? A donde vamos, no necesitaremos carreteras.”