Formation PUB010 : PHP, 2025 Les nonces

64.1 Classe Cri_Nonce pour gérer les nonces


Les nonces (Number used ONCE, c'est-à-dire numéro utilisé une seule fois) sont un mécanisme permettant de valider l'origine d'une requête HTTP.

Ils permettent d'assurer, avant d'effectuer un traitement, que l'envoi du formulaire ou le clic sur le lien ayant déclenché l'action provient bel et bien de votre site et non d'un script ou d'un lien malicieux.

Ceci permet notamment de prévenir les attaques CSRF (Cross Site Request Forgery) et les attaques par rejeu (replay attack).

Si vous souhaitez mieux comprendre ce qu'est un nonce et quel est son cycle de vie, cet article est pour vous. De plus, je vous fournis ici le code de la classe Cri_Nonce, que j'ai créée pour gérer les nonces.

Un mécanisme de validation par nonce devrait être mis en place à chaque fois qu'un formulaire doit être traité ou qu'un lien doit mener à une action sur la base de données, que le traitement soit exécuté via AJAX ou non.

La seule exception à cette règle est le cas des formulaires ou liens dont le traitement, s'il est exécuté à partir d'un script malicieux ou d'un lien contrefait, n'a aucune conséquence sérieuse.

De quoi est composé un nonce?

Le procédé consiste à créer, lors de l'affichage du formulaire ou du lien, une chaîne à partir de plusieurs informations précises comme :

  • l'identifiant de l'usager,
  • l'action à effectuer (ex : 'enregistrer', 'modifier_3', 'supprimer_8'),
  • l'identifiant de l'item sur lequel l'action doit être effectuée, s'il y a lieu (cet identifiant devrait faire partie de l'action à effectuer),
  • une clé de salage, idéalement générée aléatoirement.

Cette chaîne sera passée dans un algorithme de hachage afin d'assurer que les informations qu'elle contient ne puissent pas être lues ni modifiées. C'est alors qu'on lui donnera le nom de nonce.

Cycle de vie du nonce

Le cycle de vie du nonce sera le suivant :

  • Lors de l'affichage d'un formulaire ou d'un lien, un nonce sera généré.
  • Pour que le nonce puisse être vérifié, un mécanisme permettra de passer le nonce à la page qui effectuera le traitement du formulaire ou du lien. À cette fin, le programme se chargera de placer le nonce dans un champ caché du formulaire ou dans une variable de session. Dans le cas où le nonce permet de valider la provenance d'un lien, il sera encodé dans l'URL du lien (ex : http://mondomaine.com/produits.php?action=delete&id=36&nonce=...).
  • Avant de procéder au traitement du formulaire ou avant d'afficher la page référencée par le lien, le nonce sera recréé puis comparé avec celui provenant du formulaire ou du lien (retrouvé par $_POST, par $_GET ou par $_SESSION, selon l'endroit où il a été stocké lors de sa génération). Pour qu'il y a ait égalité, il faut d'abord qu'un nonce ait été reçu. Il faut également que la même personne soit authentifiée, que le délai soit en deçà de la période de validité du nonce, que la chaîne identifiant l'action soit la même, etc. Le traitement ne pourra donc pas avoir lieu si un faux formulaire était soumis ou si un lien était trafiqué.
  • La dernière étape du cycle de vie du nonce consiste à invalider le nonce. En effet, puisque le nonce est, par définition, un numéro utilisé une seule fois, son invalidation assurera qu'une seconde soumission du même formulaire (ex : en rafraîchissant la page de soumission) ne pourra pas mener à un second traitement.

Bibliothèques pour gérer les nonces

Il existe plusieurs bibliothèques qui offrent les fonctionnalités nécessaires pour implanter un mécanisme de nonce.

En voici quelques-unes pour vos programmes PHP :

Il est également possible de développer notre propre classe ou notre propre bibliothèque de fonctions pour gérer les nonces.

Classe Cri_Nonce

Voici la classe que j'ai développée pour gérer les nonces.

Pourquoi une nouvelle classe alors qu'il en existe déjà plusieurs ? Simplement pour bâtir un exemple concret afin de bien saisir les enjeux des nonces. Et puis cette classe maison me donne tout le contrôle désiré pour effectuer les ajustements qui pourraient s'imposer lors du développement de mes applications Web.

Table MySQL nécessaire pour utiliser la classe

Pour fonctionner, la classe nécessite :

  • un objet $mysqli, créé en dehors de la classe, qui représente la connexion au serveur MySQL;
  • dans la base de données MySQL, une table dont la structure est la suivante :
    MySQL

    CREATE TABLE nonce (
      nonce_id int NOT NULL AUTO_INCREMENT,

      nonce_valeur varchar(40) NOT NULL,

      nonce_salage varchar(32) NOT NULL,

      nonce_expiration int NOT NULL,

      PRIMARY KEY (nonce_id)

    )

    C'est grâce à cette table qu'il sera possible d'invalider le nonce dès qu'il est utilisé une fois (d'où le ONCE dans le mot nONCE).

Fonctionnement de la classe

Pour le reste de la classe, voici le fonctionnement :

  • Lors de la génération du formulaire ou du lien, on crée un nonce à l'aide de la fonction statique creer_nonce().
  • La création du nonce ajoutera automatiquement un enregistrement dans la table nonce. La fonctin creer_nonce() stockera la valeur du nonce, la clé de salage utilisée de même que l'expiration du nonce, mesurée en secondes depuis le début de l'époque UNIX (1er janvier 1970 00:00:00 GMT).
  • Vous devrez mettre en place un mécanisme pour transférer la valeur du nonce au code qui traite le formulaire ou le lien. Ce pourrait être l'utilisation d'un champ caché du formulaire, d'une variable de session ou d'un paramètre dans l'URL.
  • Lors du traitement du formulaire ou du lien, on retrouvera la valeur du nonce par $_GET, $_POST ou $_SESSION, selon la technique utilisée.
  • Avant de procéder aux modifications demandées par le formulaire ou le lien, on fera appel à la fonction statique verifier_nonce() pour s'assurer que le nonce reçu est valide. C'est cette fonction qui se chargera de récupérer l'heure d'expiration et la clé de salage utilisée lors de la génération initiale.
  • La fonction verifier_nonce() recréera le nonce puis comparera les deux versions. Pour qu'il y a ait égalité, il faut que le nonce soit présent dans la base de données, que la même personne soit authentifiée, que nonce ne soit pas expiré, que la chaîne identifiant l'action soit la même, etc. Évidemment, la génération initiale et la génération pour fin de comparaison doivent utiliser la même chaîne d'action.
  • La fonction verifier_nonce() s'occupera finalement de supprimer le nonce de la BD afin qu'il ne puisse pas être réutilisé.
  • Toutes les autres fonctions de la bibliothèque : valeur_nonce(), enregistrer_nonce_bd() et nettoyer_bd() sont utilisées à l'interne. Vous n'avez donc besoin que de creer_nonce() et verifier_nonce().

Exemple d'utilisation de la classe

Dans l'exemple suivant, on démontre l'utilisation des nonces pour protéger l'ajout de données à partir d'un formulaire.

Notez que dans l'entête de la classe, un autre exemple d'utilisation est fourni. Ce deuxième exemple démontre l'utilisation des nonces pour protéger des liens menant à la modification d'enregistrements.

Voici donc les extraits de code du premier exemple. D'abord, lors de la génération du formulaire, on créera le nonce :

PHP

$nonce = Cri_Nonce::creer_nonce('enregistrer', $_SESSION['usager_id'], $mysqli);

 

if ($nonce == '') {

    message_erreur("Un problème empêche l'affichage du formulaire.");

}

else {

?>

 

<form method="post" action="<?php echo $_SERVER['SCRIPT_NAME']; ?>">

    <input type="hidden" name="nonce" value="<?php echo $nonce; ?>"/>

    <label for="prenom">* Prénom :</label>

    <input type="text" id="prenom" name="prenom" maxlength=50 required />

    ...

    <input type="submit" value="Soumettre" name="soumettre" />

</form>

<?php

} // fin si génération du nonce a réussi

Et lors du traitement du formulaire, on vérifiera le nonce :

PHP

if (isset($_POST['soumettre'])) {

 

    $nonce_valide = false;

 

    if (isset($_POST['nonce'])) { // le nonce était, ici, stocké dans un champ caché du formulaire

        $valeur = $_POST['nonce'];

        $nonce_valide = Cri_Nonce:: verifier_nonce($valeur, 'enregistrer', $_SESSION['usager_id'], $mysqli);

    }

 

    if ($nonce_valide) {

        ... // enregistrement des données

    }

}

Code de la classe

Voici donc le code de ma classe. Ce code est bien évidemment perfectible. Si vous avez des suggestions d'amélioration, je suis preneuse !!!

PHP

/**

* Classe proposant des méthodes statiques pour gérer les nonces afin de protéger un lien ou un formulaire

* notamment contre les attaques de type XSS.

*

* Un nonce sera valide si, lors de sa vérification, le programme fournit les bonnes valeurs pour 2 variables

* qui ne sont pas stockées dans la BD : le code ou l'identifiant de l'usager et l'action.

* Ces valeurs, combinées à une clé de salage générée aléatoirement lors de la génération initiale du nonce,

* permettront de recréer le nonce original. De plus, pour être valide, le nonce ne doit pas avoir déjà été

* utilisé et il ne doit pas être expiré.

*

* Les propriétés du nonce seront stockées dans une table MySQL ayant la structure suivante :

*

* CREATE TABLE nonce (

*   nonce_id int NOT NULL AUTO_INCREMENT,

*   nonce_valeur varchar(40) NOT NULL,

*   nonce_salage varchar(32) NOT NULL,

*   nonce_expiration int NOT NULL,

*   PRIMARY KEY (nonce_id)

* )

*

* Utilisation lors de la génération initiale :

*     $nonce = Cri_Nonce::creer_nonce('modifier_$id', $user, $mysqli);

*     if ($nonce == '') {

*         echo "Un problème empêche l'affichage du lien de modification.";

*     }

*     else {

*         echo "<a href='modifier.php?id=$id&nonce=$nonce'>Modifier</a>";

*     }

*

* Utilisation lors de la vérification :

*     $valeur = $_GET['nonce'];

*     $valide = Cri_Nonce:: verifier_nonce($valeur, 'modifier_$id', $user, $mysqli);

*

* @author : Christiane Lagacé < http://christianelagace.com >

* @copyright 2016 localhost/bloguechristiane

* @license http://www.gnu.org/licenses/quick-guide-gplv3.html

* @version 1.0

*/

 

class Cri_Nonce {

 

    /**

     * @var int DUREE_SECONDES Durée de validité du nonce, en secondes.

     */

    const DUREE_SECONDES = 7200;

 

    // ***********************************************************

    /**

     * Créer le nonce.

     *

     * @param string $action Chaîne de caractères qui identifie l'action que le formulaire ou le lien doit faire. La valeur de cette chaîne a peu d'importance. Son rôle est d'augmenter la sécurité du nonce. Dans le cas où le nonce sert à protéger un lien menant à une action sur un item de la BD (ex : lien pour supprimer un produit), la clé devrait inclure l'identifiant de cet item (ex : 'supprimer_8', 'enregistrer', 'modifier_3').

     * @param string $usager Code ou identifiant de l'usager qui était authentifié lors de la création initiale ou lors de la vérification du nonce.

     * @param mysqli $mysqli Objet mysqli qui permettra d'accéder à la base de données pour enregistrer ou retrouver des valeurs dans la table nonce.

 

     * @return string Valeur du nonce ou '' si la création n'a pas réussi.

     */

    public static function creer_nonce($action, $usager, $mysqli) {

        // sous PHP 7, il est possible d'utiliser random_bytes(), qui, selon la doc : Generates cryptographically secure pseudo-random bytes

        if (function_exists('random_bytes')) {

            $salage = random_bytes(32);

        }

        else {

            // attention : avec openssl_random_pseudo_bytes(), il arrive que l'algorithme fort de cryptologie ne puisse pas être été utilisé

            if (function_exists('openssl_random_pseudo_bytes')) {

                $salage = openssl_random_pseudo_bytes(32);

            }

            // à défaut d'une meilleure solution, on utilise une fonction qui ne génère pas de valeurs sécurisées d'un point de vue cryptologie

            else {

                $salage = mt_rand();

            }

        }

 

        $expiration = time() + self::DUREE_SECONDES;

        $valeur = self::valeur_nonce($action, $usager, $salage);

 

        if (!self::enregistrer_nonce_bd($valeur, $salage, $expiration, $mysqli)) {

            $valeur = '';

        }

 

        return $valeur;

    }

 

    // ***********************************************************

    /**

     * Vérifier si le nonce est valide en créant un nouveau nonce et en le comparant avec la valeur originale.

     *

     * @param string $valeur Valeur du nonce.

     * @param string $action Chaîne de caractères qui identifie l'action que le formulaire ou le lien doit faire.

     * @param string $usager Code ou identifiant de l'usager qui était authentifié lors de la vérification du nonce.

     * @param mysqli $mysqli Objet mysqli qui permettra d'accéder à la base de données pour enregistrer ou retrouver des valeurs dans la table nonce.

     *

     * @return bool Indique si le nonce est valide.

     */

    public static function verifier_nonce($valeur, $action, $usager, $mysqli) {

        $valide = false;

 

        // retrouver les informations sur le nonce dans la base de données

        $reussi = false;

 

        $requete = "SELECT nonce_salage, nonce_expiration FROM nonce WHERE nonce_valeur = ?";

        $stmt = $mysqli->prepare($requete);

 

        if ($stmt) {

 

            $stmt->bind_param("s", $valeur);

 

            $stmt->execute();

            $stmt->bind_result($salage, $expiration);

            $stmt->fetch();

 

            if ($stmt->num_rows != -1) {

                $reussi = true;

            }

 

            $stmt->close();

        }

 

        if ($reussi) {

            // si le nonce n'a pas expiré

            if ($expiration > time()) {

 

                // recrée le nonce pour pouvoir le comparer à l'original

                $nouvelle_valeur = self::valeur_nonce($action, $usager, $salage);

 

                if ($nouvelle_valeur == $valeur) {

                    $valide = true;

                }

            }

 

            // supprime le nonce de la BD ainsi que tous les nonces qui sont expirés

            self::nettoyer_bd($valeur, $mysqli);

        }

 

        return $valide;

    }

 

    // ***********************************************************

    /**

     * Monter la valeur du nonce.

     *

     * @param string $action Chaîne de caractères qui identifie l'action que le formulaire ou le lien doit faire.

     * @param string $usager Code ou identifiant de l'usager qui était authentifié lors de la création initiale ou lors de la vérification du nonce.

     * @param string $salage Clé de salage utilisée lors de la génération initiale.

 

     * @return string Valeur du nonce.

     */

    private static function valeur_nonce($action, $usager, $salage) {

        return sha1($action . $usager . $salage);

    }

 

    // ***********************************************************

    /**

     * Enregistrer le nonce dans la base de données

     *

     * @param string $valeur Valeur du nonce.

     * @param string $salage Clé de salage utilisée lors de la génération initiale.

     * @param int $expiration Expiration du nonce, mesurée en secondes depuis le début de l'époque UNIX (1er janvier 1970 00:00:00 GMT).

     * @param mysqli $mysqli Objet mysqli qui permettra d'accéder à la base de données pour enregistrer les données dans la table nonce.

     *

     * @return bool Indique si l'enregistrement a réussi.

     */

    private static function enregistrer_nonce_bd($valeur, $salage, $expiration, $mysqli) {

 

        $reussi = false;

 

        $requete = "INSERT INTO nonce(nonce_valeur, nonce_salage, nonce_expiration) VALUES (?, ?, ?)";

        $stmt = $mysqli->prepare($requete);

 

        if ($stmt) {

 

            $stmt->bind_param("ssd", $valeur, $salage, $expiration);

 

            $stmt->execute();

 

            if ($stmt->affected_rows != -1) {

                $reussi = true;

            }

 

            $stmt->close();

        }

 

        return $reussi;

    }

 

    // ***********************************************************

    /**

     * Supprimer le nonce de la base de données, ainsi que tous les nonces qui sont expirés.

     *

     * @param string $valeur Valeur du nonce.

     * @param mysqli $mysqli Objet mysqli qui permettra d'accéder à la base de données pour supprimer des enregistrements de la table nonce.

     *

     * @return bool Indique si la suppression a réussi.

     */

    private static function nettoyer_bd($valeur, $mysqli) {

 

        $reussi = false;

 

        $requete = "DELETE FROM nonce WHERE nonce_expiration <= ? OR nonce_valeur = ?";

        $stmt = $mysqli->prepare($requete);

 

        if ($stmt) {

            $time = time();

 

            $stmt->bind_param("ds", $time, $valeur);

 

            $stmt->execute();

 

            if ($stmt->affected_rows != -1) {

                $reussi = true;

            }

 

            $stmt->close();

        }

 

        return $reussi;

    }

 

} // fin de la définition de la classe

Pour plus d'information

« Cryptographic nonce ». Wikipedia. https://en.wikipedia.org/wiki/Cryptographic_nonce

« The Word of the Day Is "Nonce" ». HTML Goodies. http://www.htmlgoodies.com/beyond/javascript/the-word-of-the-day-is-nonce.html

« How to create and use nonces ». Stack Overflow. http://stackoverflow.com/questions/4145531/how-to-create-and-use-nonces

« A Simple Form Nonce Security Routine Written In PHP ». Doug Vanderweide. https://www.dougv.com/2014/01/a-simple-form-nonce-security-routine-written-in-php/

▼Publicité

Veuillez noter que le contenu de cette fiche vous est partagé à titre gracieux, au meilleur de mes connaissances et sans aucune garantie.
Merci de partager !
Soumettre