Comment ajouter correctement un jeton de falsification de demande intersite (CSRF) à l'aide de PHP

96

J'essaye d'ajouter une certaine sécurité aux formulaires sur mon site Web. L'un des formulaires utilise AJAX et l'autre est un simple formulaire «contactez-nous». J'essaye d'ajouter un jeton CSRF. Le problème que j'ai est que le jeton n'apparaît que dans la "valeur" HTML de temps en temps. Le reste du temps, la valeur est vide. Voici le code que j'utilise sur le formulaire 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>

Aucune suggestion?

Ken
la source
Juste curieux, à quoi token_timeça sert?
zerkms
@zerkms que je n'utilise pas actuellement token_time. J'allais limiter le temps pendant lequel un jeton est valide, mais je n'ai pas encore entièrement implémenté le code. Par souci de clarté, je l'ai supprimé de la question ci-dessus.
Ken le
1
@Ken: pour que l'utilisateur puisse obtenir le cas lorsqu'il a ouvert un formulaire, le publier et obtenir un jeton invalide? (depuis qu'il a été invalidé)
zerkms
@zerkms: Merci, mais je suis un peu confus. Avez-vous une chance de me donner un exemple?
Ken le
2
@Ken: bien sûr. Supposons que le jeton expire à 10h00. Il est maintenant 09h59. L'utilisateur ouvre un formulaire et obtient un jeton (qui est toujours valide). Ensuite, l'utilisateur remplit le formulaire pendant 2 minutes et l'envoie. Tant qu'il est 10h01 maintenant - le jeton est traité comme invalide, donc l'utilisateur obtient une erreur de formulaire.
zerkms

Réponses:

290

Pour le code de sécurité, veuillez ne pas générer vos jetons de cette façon: $token = md5(uniqid(rand(), TRUE));

Essayez ceci:

Générer un jeton CSRF

PHP 7

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

Sidenote: L'un des projets open source de mon employeur est une initiative de backport random_bytes()et de random_int()projets PHP 5. Il est sous licence MIT et disponible sur Github et Composer en tant que paragonie / random_compat .

PHP 5.3+ (ou avec 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'];

Vérification du jeton CSRF

Ne vous contentez pas d'utiliser ==ou même d' ===utiliser hash_equals()(PHP 5.6+ uniquement, mais disponible pour les versions antérieures avec la bibliothèque 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
    }
}

Aller plus loin avec les jetons par formulaire

Vous pouvez restreindre davantage les jetons afin qu'ils ne soient disponibles que pour un formulaire particulier en utilisant hash_hmac(). HMAC est une fonction de hachage à clé particulière qui peut être utilisée en toute sécurité, même avec des fonctions de hachage plus faibles (par exemple MD5). Cependant, je recommande d'utiliser la famille de fonctions de hachage SHA-2 à la place.

Tout d'abord, générez un deuxième jeton à utiliser comme clé HMAC, puis utilisez une logique comme celle-ci pour le rendre:

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

Et puis en utilisant une opération congruente lors de la vérification du jeton:

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

Les jetons générés pour un formulaire ne peuvent pas être réutilisés dans un autre contexte sans le savoir $_SESSION['second_token']. Il est important que vous utilisiez un jeton distinct comme clé HMAC que celui que vous venez de déposer sur la page.

Bonus: approche hybride + intégration Twig

Quiconque utilise le moteur de création de modèles Twig peut bénéficier d'une double stratégie simplifiée en ajoutant ce filtre à son environnement 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']);
        }
    )
);

Avec cette fonction Twig, vous pouvez utiliser les deux jetons à usage général comme ceci:

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

Ou la variante verrouillée:

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

Twig ne s'occupe que du rendu des modèles; vous devez toujours valider correctement les jetons. À mon avis, la stratégie Twig offre une plus grande flexibilité et simplicité, tout en conservant la possibilité d'une sécurité maximale.


Jetons CSRF à usage unique

Si vous avez une exigence de sécurité selon laquelle chaque jeton CSRF est autorisé à être utilisable exactement une fois, la stratégie la plus simple le régénère après chaque validation réussie. Cependant, cela invalidera chaque jeton précédent qui ne se marie pas bien avec les personnes qui parcourent plusieurs onglets à la fois.

Paragon Initiative Enterprises gère une bibliothèque Anti-CSRF pour ces cas d'angle. Il fonctionne avec des jetons à usage unique par formulaire, exclusivement. Lorsque suffisamment de jetons sont stockés dans les données de session (configuration par défaut: 65535), les jetons non utilisés les plus anciens sont d'abord supprimés.

Scott Arciszewski
la source
sympa, mais comment changer le jeton $ après que l'utilisateur ait soumis le formulaire? dans votre cas, un jeton utilisé pour la session utilisateur.
Akam
1
Regardez attentivement comment github.com/paragonie/anti-csrf est implémenté. Les jetons sont à usage unique, mais ils en stockent plusieurs.
Scott Arciszewski
@ScottArciszewski À quoi pensez-vous pour générer un résumé de message à partir de l'identifiant de session avec un secret, puis comparer le résumé de jeton CSRF reçu avec à nouveau le hachage de l'identifiant de session avec mon secret précédent? J'espère que tu comprends ce que je veux dire.
MNR
1
J'ai une question sur la vérification du jeton CSRF. Si $ _POST ['token'] est vide, nous ne devrions pas continuer, car la demande de ce message a été envoyée sans le jeton, non?
Hiroki
1
Parce qu'il va être repris dans le formulaire HTML, et vous voulez qu'il soit imprévisible afin que les attaquants ne puissent pas simplement le falsifier. Vous implémentez vraiment l'authentification par défi-réponse ici, pas simplement "oui, ce formulaire est légitime" parce qu'un attaquant peut simplement usurper cela.
Scott Arciszewski
24

Avertissement de sécurité : ce md5(uniqid(rand(), TRUE))n'est pas un moyen sûr de générer des nombres aléatoires. Consultez cette réponse pour plus d'informations et une solution qui exploite un générateur de nombres aléatoires cryptographiquement sécurisé.

On dirait que vous avez besoin d'un autre avec votre if.

if (!isset($_SESSION['token'])) {
    $token = md5(uniqid(rand(), TRUE));
    $_SESSION['token'] = $token;
    $_SESSION['token_time'] = time();
}
else
{
    $token = $_SESSION['token'];
}
données
la source
11
Remarque: je ne ferais pas confiance md5(uniqid(rand(), TRUE));aux contextes de sécurité.
Scott Arciszewski
2

La variable $tokenn'est pas récupérée de la session lorsqu'elle s'y trouve

Dani
la source