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.
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 :
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.
Le cycle de vie du nonce sera le suivant :
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.
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.
Pour fonctionner, la classe nécessite :
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).
Pour le reste de la classe, voici le fonctionnement :
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 :
$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 :
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
}
}
Voici donc le code de ma classe. Ce code est bien évidemment perfectible. Si vous avez des suggestions d'amélioration, je suis preneuse !!!
/**
* 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
« 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é