Cómo agregar correctamente el token de falsificación de solicitudes entre sitios (CSRF) usando PHP


96

Estoy tratando de agregar algo de seguridad a los formularios de mi sitio web. Uno de los formularios utiliza AJAX y el otro es un sencillo formulario de "contacto". Estoy intentando agregar un token CSRF. El problema que tengo es que el token solo aparece en el "valor" de HTML algunas veces. El resto del tiempo, el valor está vacío. Aquí está el código que estoy usando en el formulario AJAX:

PHP:

if (!isset($_SESSION)) {
    session_start();
$_SESSION['formStarted'] = true;
}
if (!isset($_SESSION['token']))
{$token = md5(uniqid(rand(), TRUE));
$_SESSION['token'] = $token;

}

HTML

 <form>
//...
<input type="hidden" name="token" value="<?php echo $token; ?>" />
//...
</form>

¿Alguna sugerencia?


Solo curiosidad, ¿ token_timepara qué se usa?
zerkms

@zerkms no estoy usando actualmente token_time. Iba a limitar el tiempo dentro del cual un token es válido, pero aún no he implementado completamente el código. En aras de la claridad, lo eliminé de la pregunta anterior.
Ken

1
@Ken: ¿para que el usuario pueda obtener el caso cuando abrió un formulario, publicarlo y obtener un token no válido? (ya que ha sido invalidado)
zerkms

@zerkms: Gracias, pero estoy un poco confundido. ¿Alguna posibilidad de que me pueda dar un ejemplo?
Ken

2
@Ken: seguro. Supongamos que el token expira a las 10:00 a. M. Ahora son las 09:59 am. El usuario abre un formulario y obtiene un token (que sigue siendo válido). Luego, el usuario llena el formulario durante 2 minutos y lo envía. Siempre que sean las 10:01 a.m. ahora, el token se trata como no válido, por lo que el usuario recibe un error de formulario.
zerkms

Respuestas:


291

Para el código de seguridad, no genere sus tokens de esta manera: $token = md5(uniqid(rand(), TRUE));

Probar esto:

Generando un token CSRF

PHP 7

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

Nota al margen: Uno de los proyectos de código abierto de mi empleador es una iniciativa de backport random_bytes()y random_int()en proyectos PHP 5. Tiene licencia del MIT y está disponible en Github y Composer como paragonie / random_compat .

PHP 5.3+ (o con ext-mcrypt)

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'];

Verificación del token CSRF

No solo use ==o incluso ===use hash_equals()(solo PHP 5.6+, pero disponible para versiones anteriores con la biblioteca hash-compat ).

if (!empty($_POST['token'])) {
    if (hash_equals($_SESSION['token'], $_POST['token'])) {
         // Proceed to process the form data
    } else {
         // Log this as a warning and keep an eye on these attempts
    }
}

Yendo más lejos con tokens por formulario

Puede restringir aún más los tokens para que solo estén disponibles para un formulario en particular usando hash_hmac(). HMAC es una función hash con clave particular que es segura de usar, incluso con funciones hash más débiles (por ejemplo, MD5). Sin embargo, recomiendo usar la familia SHA-2 de funciones hash en su lugar.

Primero, genere un segundo token para usar como clave HMAC, luego use una lógica como esta para representarlo:

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

Y luego usando una operación congruente al verificar el token:

$calc = hash_hmac('sha256', '/my_form.php', $_SESSION['second_token']);
if (hash_equals($calc, $_POST['token'])) {
    // Continue...
}

Los tokens generados para un formulario no se pueden reutilizar en otro contexto sin saberlo $_SESSION['second_token']. Es importante que utilice un token diferente como clave HMAC que el que acaba de colocar en la página.

Bono: Enfoque híbrido + Integración Twig

Cualquiera que use el motor de plantillas Twig puede beneficiarse de una estrategia dual simplificada al agregar este filtro a su entorno Twig:

$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']);
        }
    )
);

Con esta función Twig, puede usar ambos tokens de propósito general así:

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

O la variante bloqueada:

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

Twig solo se ocupa del renderizado de plantillas; aún debe validar los tokens correctamente. En mi opinión, la estrategia Twig ofrece una mayor flexibilidad y simplicidad, al tiempo que mantiene la posibilidad de máxima seguridad.


Tokens CSRF de un solo uso

Si tiene un requisito de seguridad de que cada token CSRF se pueda usar exactamente una vez, la estrategia más simple lo regenerará después de cada validación exitosa. Sin embargo, hacerlo invalidará todos los tokens anteriores, lo que no se combina bien con las personas que navegan por varias pestañas a la vez.

Paragon Initiative Enterprises mantiene una biblioteca Anti-CSRF para estos casos de esquina. Funciona con tokens de un solo uso por formulario, exclusivamente. Cuando se almacenan suficientes tokens en los datos de la sesión (configuración predeterminada: 65535), se eliminarán primero los tokens no canjeados más antiguos.


bueno, pero ¿cómo cambiar el $ token después de que el usuario envió el formulario? en su caso, un token utilizado para la sesión del usuario.
Akam

1
Mire de cerca cómo se implementa github.com/paragonie/anti-csrf . Los tokens son de un solo uso, pero almacenan varios.
Scott Arciszewski

@ScottArciszewski ¿Qué piensas para generar un resumen del mensaje a partir de la identificación de la sesión con un secreto y luego comparar el resumen del token CSRF recibido con el hash de la identificación de la sesión con mi secreto anterior? Espero entiendas lo que quiero decir.
MNR

1
Tengo una pregunta sobre la verificación del token CSRF. Si $ _POST ['token'] está vacío, no deberíamos continuar, porque la solicitud de esta publicación se envió sin el token, ¿verdad?
Hiroki

1
Porque se repetirá en el formulario HTML, y desea que sea impredecible para que los atacantes no puedan simplemente falsificarlo. Realmente está implementando la autenticación de desafío-respuesta aquí, no simplemente "sí, este formulario es legítimo" porque un atacante puede simplemente falsificarlo.
Scott Arciszewski

24

Advertencia de seguridad : md5(uniqid(rand(), TRUE))no es una forma segura de generar números aleatorios. Consulte esta respuesta para obtener más información y una solución que aprovecha un generador de números aleatorios criptográficamente seguro.

Parece que necesitas un else con tu if.

if (!isset($_SESSION['token'])) {
    $token = md5(uniqid(rand(), TRUE));
    $_SESSION['token'] = $token;
    $_SESSION['token_time'] = time();
}
else
{
    $token = $_SESSION['token'];
}

11
Nota: No confiaría md5(uniqid(rand(), TRUE));en contextos de seguridad.
Scott Arciszewski

2

La variable $tokenno se recupera de la sesión cuando está allí

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.