Version en ligne

Tutoriel : Sécurisation des failles CSRF

Table des matières

Sécurisation des failles CSRF
CSRF, késako?
Le jeton de sécurité ou token
Le referer
Autres conseils
Les bonus

Sécurisation des failles CSRF

CSRF, késako?

Bienvenue à tous !

Aujourd'hui, je vais vous introduire une nouvelle faille de sécurité, et vous expliquer comment vous en protéger au mieux.

Si vous êtes développeur et que la notion de "token" ne vous dit rien, lisez vite ce tuto, on est peut-être déjà en train de vous pirater !

C'est partiiiii ! ;)

CSRF, késako?

Le jeton de sécurité ou token

Je suis sûr que vous vous demandez :

Mais c'est quoi ce CSRF dont il nous parle depuis tout à l'heure ?

Ah oui, j'avais oublié de vous expliquer ! :diable:

CSRF signifie "Cross-Site Request Forgeries". Je vous donne un exemple :
Paul est newser d'un site, il peut donc ajouter, modifier, et supprimer une news, ce qu'un utilisateur du site lambda ne peut PAS faire.
Jean est un de ces utilisateurs lambda, et il aimerait pirater le site de Paul ! :pirate: Il va donc récupérer l'adresse permettant de supprimer une news, et envoyer un message privé à Paul contenant une image dont l'adresse sera celle de la page de suppression de news.

Et là, le navigateur entre en jeu. En essayant d'afficher l'image, il va aller sur la page web permettant de supprimer la news et donc l'exécuter. Or, Paul était identifié en tant que newser, la news sera donc supprimée car IL a affiché la page.

Sur le même principe, si Paul peut recevoir une requête ajax qui en s'exécutant va poster ou modifier une news !
C'est-à-dire que Paul recevra un code javascript qui "forcera" le navigateur à appeler la page que le pirate veut que Paul exécute.

On va donc apprendre à se protéger de ces failles très dangereuses, sachant qu'il n'existe pas de protection parfaite.

Avant d'entrer dans le vif du sujet, je précise à toutes fins utiles que cette faille ne s'applique pas uniquement au cas que je présente dans ce tuto (c'est à dire le "piratage" d'un espace d'administration), mais peut-être utilisée dans de nombreux domaines où le but est de faire générer une requête par le navigateur de la victime (pour truquer des votes par exemple).


Le jeton de sécurité ou token

Le jeton de sécurité ou token

CSRF, késako? Le referer

Alors, pour commencer, voyons la protection la plus courante. :)
Elle consiste à stocker un jeton unique (clairement une suite de nombres et de lettres) associé à la date d'affichage pour chaque visiteur qui affiche un formulaire dans une session, et ce même jeton dans un champ caché. Ceci permet que la personne qui tente d'exécuter la page est bien passée par le formulaire avant, où on lui a délivré le jeton.

Ce qui donne :

<?php
//On démarre les sessions
session_start();
//On génére un jeton totalement unique (c'est capital :D)
$token = uniqid(rand(), true);
//Et on le stocke
$_SESSION['token'] = $token;
//On enregistre aussi le timestamp correspondant au moment de la création du token
$_SESSION['token_time'] = time();

//Maintenant, on affiche notre page normalement, le champ caché token en plus
?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Mon formulaire anti CSRF</title>
</head>

<body>
<form id="form" name="form" method="post" action="traitement.php">
  <p>Pseudo : 
    <label>
      <input type="text" name="pseudo" id="pseudo" />
    </label>
  </p>
  <p>E-mail : 
    <label>
      <input type="text" name="email" id="email" />
    </label>
  </p>
  <p>Nom : 
    <label>
      <input type="text" name="nom" id="nom" />
    </label>
    <input type="hidden" name="token" id="token" value="<?php
//Le champ caché a pour valeur le jeton
echo $token;
	?>"/>
  </p>
  <p>
    <label>
      <input type="submit" name="Envoyer" id="Envoyer" value="Envoyer" />
    </label>
  </p>
</form>
</body>
</html>

Bon, jusque-là, c'est bon je pense... Les commentaires sont explicites. :soleil:

Ensuite, on passe à la page du traitement, et vous vous rendrez compte qu'il est très simple de se protéger efficacement.

On va déjà vérifier la présence du token, de sa date dans la session, et du token envoyé par POST. Donc, si la personne qui exécute la page n'est pas passée par le formulaire, ça bloque...

Le token doit aussi être valide, c'est-à-dire identique à celui envoyé par POST, et non-expiré, c'est-à-dire que sa génération ne remonte pas à trop longtemps.

Puis on vérifie aussi si le token de $_POST est le même que celui de $_SESSION. Ce qui donne finalement :

<?php
session_start();
//On va vérifier :
//Si le jeton est présent dans la session et dans le formulaire
if(isset($_SESSION['token']) && isset($_SESSION['token_time']) && isset($_POST['token']))
{
	//Si le jeton de la session correspond à celui du formulaire
	if($_SESSION['token'] == $_POST['token'])
	{
		//On stocke le timestamp qu'il était il y a 15 minutes
		$timestamp_ancien = time() - (15*60);
		//Si le jeton n'est pas expiré
		if($_SESSION['token_time'] >= $timestamp_ancien)
		{
				//ON FAIT TOUS LES TRAITEMENTS ICI
				//...
				//...
		}
	}
}
//SINON, ON RAJOUTE DES ELSE ET DES MESSAGES D'ERREUR
?>

Voilà pour la protection de base. Suivez le guide, on va rentrer dans d'autres petites sécurités à mettre en place pour bien gêner le pirate. :diable:

Évidemment, on peut utiliser un token pour des actions qui ne passent pas par un formulaire.
Par exemple : sur une page ou l'administrateur peut supprimer des news, il y a plusieurs liens vers : supprimer_news.php?id=34

Au lieu de transmettre le jeton par POST, on le transmet par GET, comme les autres données (en l'occurence l'ID de la news à supprimer). Le principe reste sinon le même, mais le POST est plus sûr.


CSRF, késako? Le referer

Le referer

Le jeton de sécurité ou token Autres conseils

On va donc poursuivre notre long chemin vers la vérité. ;)

Imaginons que le pirate fasse une injection CSRF et que, au moyen de requêtes AJAX, il fasse afficher à l'admin la page du formulaire puis la page du traitement...

Une requête AJAX, c'est un code Javascript qui s'exécute et qui appelle en arrière plan (c'est à dire que c'est invisible pour l'utilisateur) une page distante. Le javascript étant un langage client, c'est à dire exécuté par l'utilisateur et non par le serveur, le site qui reçoit la requête AJAX est appelé comme si l'utilisateur victime l'avait appelé consciemment. C'est donc un moyen de réaliser une attaquer CSRF. Je vous renvoie vers ce tuto.

Eh bien l'injection serait réussie :waw: , car le token aurait bien été généré par la page 1, et la vérification sera passée avec succès sur la page 2, sauf si vous vérifiez que la page qui a conduit le visiteur à la page de traitement est bien le formulaire, c'est à dire que le visiteur a cliqué sur un lien ou un bouton de la page 1 qui l'a directement conduit à la page 2.

On peut vraiment faire ça ? :honte:

Oui, bien sûr, grâce à $_SERVER['HTTP_REFERER']. C'est une variable disponible n'importe où dans votre script sans rien à faire de particulier (comme toutes ses cousines $_SERVER['...']), et elle contient l'adresse, si elle existe, de la page qui a amené votre visiteur sur le script en cours.

Donc, on va rajouter un if qui vérifiera si cette variable est égale à la page formulaire.php, je vous laisse trouver ça tout seul (je sais, je suis cruel :diable: ).

Ramassage des copies, et correction :

<?php
session_start();
//On va vérifier :
//Si le jeton est présent dans la session et dans le formulaire
if(isset($_SESSION['token']) && isset($_SESSION['token_time']) && isset($_POST['token']))
{
	//Si le jeton de la session correspond à celui du formulaire
	if($_SESSION['token'] == $_POST['token'])
	{
		//On stocke le timestamp qu'il était il y a 15 minutes
		$timestamp_ancien = time() - (15*60);
		//Si le jeton n'est pas expiré
		if($_SESSION['token_time'] >= $timestamp_ancien)
		{
			//Si le referer est bon
			if($_SERVER['HTTP_REFERER'] == 'http://monserveur.com/leformulaire.php')
			{
				//ON FAIT TOUS LES TRAITEMENTS ICI
				//...
				//...
			}
		}
	}
}
//SINON, ON RAJOUTE DES ELSE ET DES MESSAGES D'ERREUR
?>

Remplacez évidemment l'adresse du formulaire par la bonne. :)

Voilà, vous êtes déjà bien protégés, mais il y a encore quelques petites choses à voir... On continue!


Le jeton de sécurité ou token Autres conseils

Autres conseils

Le referer Les bonus

Donc, premièrement, passez le plus de données possibles en POST, ça oblige à monter une attaque avec des requêtes AJAX, puisque la technique de l'image (voir partie I) ne permet pas de passer des données en POST à la page appelée, mais seulement en GET (dans l'URL).
De plus, la taille des URL est limitée à 255 caractères, donc ça fait pas lourd.
Privilégiez le POST, mais avec discernement, pour passer un id dans l'URL, par exemple, profil.php?id_membre=1, pas la peine d'utiliser POST, GET suffira bien.

Ensuite, on a obligé le pirate à nous envoyer du code AJAX qui sera exécuté par l'administrateur. Hé, il me vient une idée, si on l'empêchait de le faire ? Allez, lançons-nous ! :magicien:

On va échapper les caractères HTML nécessaires par exemple le < et les ' " qui sont indispensables pour ce type d'attaque (ne soupirez pas, c'est plus que simplissime). La plupart de vous doit s'en douter, on va utiliser htmlspecialchars().

Il suffit donc de passer tout ce qui risque de contenir du code qui sera affiché à travers cette fonction, par exemple (vous pouvez changer le nom des variables :D ):

<?php
$chaine_securisee = htmlentities($chaine_a_risque);
?>

À la poursuite de la protection parfaite :ange: , vous pouvez aussi sur la page traitement demander une confirmation avant de valider l'action. Donc, à moins que le webmaster clique sur poursuivre, rien ne sera exécuté. Mais réservez cette technique aux points clés du site, parce que c'est très lourd !
Dans le même ton, il est possible d'exiger une double connexion pour obtenir l'accès à la partie d'administration, comme dans certains forums PhPBB. Ainsi, on peut conserver une option de connexion automatique (bien pratique) sans risque, puisque si l'administrateur veut se connecter à l'administration, il devra se reconnecter.

Vous pouvez aussi sur le même principe demander avant une action critique de retaper le mot de passe.

Enfin, je vous conseille de lire ce tuto, qui vous donnera des pistes pour contrer le vol de session, qui ici peut constituer une faille dans le système.

Il existe donc de nombreux moyens de vous protéger, je ne les ai pas tous cités, et je vous invite donc à vous documenter, puisque le Web ne manque pas d'informations à ce sujet, et de jeter un coup d'oeil à la partie bonus ! L'idéal est de choisir une ou un ensemble de protections qui combinent praticité et efficacité, c'est à dire le compromis entre sécurité et ergonomie. Là, vous êtes les seuls juges.


Le referer Les bonus

Les bonus

Autres conseils

Résumé de nos fonctions

Vous êtes encore là ? Très bien, pour vous féliciter de votre persévérance, je vous ai récapitulé tout le code, évidemment, à vous de l'adapter à vos besoins.

Voici donc 2 petites fonctions qui récapitulent les principales protections, pour les explications référez-vous aux commentaires :

<?php

//Cette fonction génère, sauvegarde et retourne un token
//Vous pouvez lui passer en paramètre optionnel un nom pour différencier les formulaires
function generer_token($nom = '')
{
	session_start();
	$token = uniqid(rand(), true);
	$_SESSION[$nom.'_token'] = $token;
	$_SESSION[$nom.'_token_time'] = time();
	return $token;
}


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


//Cette fonction vérifie le token
//Vous passez en argument le temps de validité (en secondes)
//Le referer attendu (adresse absolue, rappelez-vous :D)
//Le nom optionnel si vous en avez défini un lors de la création du token
function verifier_token($temps, $referer, $nom = '')
{
session_start();
if(isset($_SESSION[$nom.'_token']) && isset($_SESSION[$nom.'_token_time']) && isset($_POST['token']))
	if($_SESSION[$nom.'_token'] == $_POST['token'])
		if($_SESSION[$nom.'_token_time'] >= (time() - $temps))
			if($_SERVER['HTTP_REFERER'] == $referer)
				return true;
return false;
}
?>

Bon, je vous donne quand même un exemple d'utilisation mes chers Zéros. Voici la page du formulaire :

<?php
$token = generer_token('forum');
//Ensuite, le formulaire normal, pensez au champ caché. ;)
?>

Et voici la page de traitement :

<?php
if(verifier_token(600, 'http://test.fr/formulaire.php', 'forum'))
{
	//TRAITEMENTS
	//..
	//..
}
else
{
	//ERREUR	
}
?>

Mais pourquoi met-on if(verifier_token(...)) ? Ça n'a aucun sens ?

Eh bien si : la fonction renvoie true si le token est valide, et false s'il est invalide, ce qui donne :
Si le token est validé : if(true), ce qui équivaut à "si vrai", donc la condition est vérifiée
Sinon, c'est if(false), si faux, condition non valide, on exécute le else.

Pour approfondir

Si vous voulez en apprendre plus, je vous conseille les sites suivants :

Voilà, c'est fini ! :'(

Vous avez appris à vous protéger contre un type de faille assez méconnu chez les débutants. N'hésitez pas à poster un commentaire si vous avez un problème, j'y répondrai, mais pas MP par contre. ;)

Avant de nous quitter, je tiens à remercier les différents Zér0s qui m'ont aidé par leurs commentaires à ajouter, modifier et améliorer certains points du tuto. Je vous encourage aussi à me faire part de vos commentaires, pour pouvoir avancer. ;)

Enfin, je précise, pour éviter les critiques acerbes de certains, que ce tutoriel n'est qu'une introduction aux failles CSRF. Il ne les traite pas exhaustivement, l'intérêt étant surtout d'attirer l'attention des webmestres débutants sur le fait qu'il existe des failles très dangereuses auxquelles on ne pense pas immédiatement, CSRF en étant un bon exemple.

Merci de votre lecture, à bientôt!


Autres conseils