8722 sujets

Développement web côté serveur, CMS

Pages :
Bonjour,

Je cherche à sécuriser les formulaires de mon site. J'ai donc lu pas mal de tutoriels sur le sujet (et ils ne manquent pas !)
Néanmoins, il me semble que la méthode de sécurisation par jeton (ou "token") ne s'applique pas à mon cas... mais je n'en suis pas sûr, et j'aimerais en avoir la confirmation.

Lorsque je crée un formulaire sur mon site, je fais le traitement de ce formulaire sur la même page, avec un code proche de celui-ci :
<?php if (isset($_POST["update"]) && !empty($_POST["email"])) {
	// Ici mes requêtes permettant la mise à jour de la BDD
} ?>
<html>
	<body>
		<form action="<?php echo htmlspecialchars($_SERVER['REQUEST_URI'], ENT_QUOTES); ?>" method="post">
			<fieldset>
				<label for="email">Votre nouvelle adresse e-mail :</label>
				<input type="text" name="email" id="email" size="20" maxlength="40" value="<?php if (!empty($_POST["email"])) { echo htmlspecialchars($_POST["email"], ENT_QUOTES); } ?>" />
				<input type="submit" name="update" class="button" value="Mettre à jour" />
			</fieldset>
		</form>
	</body>
</html>

La partie isset($_POST["update"]) assure bien que l'on ait validé le formulaire, et rend donc inutile l'ajout d'un jeton de sécurité, non ?
Merci à ceux qui pourront me le confirmer...
Modifié par Fix (22 Nov 2013 - 21:27)
"La partie isset($_POST["update"]) assure bien que l'on ait validé le formulaire"

Oui mais un jeton sert à certifier que c'est bien TOI (l'utilisateur) qui a demandé a effectué l'action. Ce n'est pas la même chose que de savoir si le formulaire a été validé.
Je comprends. Mais est-ce que ça a un sens de créer le jeton, de l'envoyer et de le vérifier, le tout sur la même page ? Je ne parviens pas à imaginer comment le jeton pourrait ne pas être correct...
Fix a écrit :
Je comprends. Mais est-ce que ça a un sens de créer le jeton, de l'envoyer et de le vérifier, le tout sur la même page ? Je ne parviens pas à imaginer comment le jeton pourrait ne pas être correct...


Le jeton se toujours correct si l'utilisateur passe par ton formulaire, il ne le sera pas si on le force à faire une action depuis une autre page de ton site ou un autre site. C'est le principe des attaques CSRF.
Je comprends toujours assez mal comment, dans mon cas, on pourrait forcer mon script à faire une action depuis une autre page, étant donné qu'il vérifie si le formulaire de ladite page a été validé avant d'accomplir quoi que ce soit.
J'ai beau relire les exemples du site du zéro ou d'autres tutos, ça reste peu clair... Merci quand même !

loicbcn > tu as raison, ça fout les jetons Smiley cligne
Ta vérification pour voir si le formulaire est posté c'est une simple variable POST au même titre que ta variable email donc il n'y aucune difficulté à envoyer cette valeur pour simuler la soumission du formulaire. Ce que je veux dire c'est qu'au niveau sécurité ta variable $_POST['update'] ne sert à rien, contrairement à un token.
a écrit :
Je comprends toujours assez mal comment, dans mon cas, on pourrait forcer mon script à faire une action depuis une autre page, étant donné qu'il vérifie si le formulaire de ladite page a été validé avant d'accomplir quoi que ce soit.


N'importe qui peut forger une requête HTTP valide et peut te faire croire que ton formulaire a bien été envoyé correctement. Le token de sécurité sert à ce qu'on soit obligé d'ouvrir ta page contenant le formulaire avant de l'envoyer effectivement. Ca prémunit pas des script kiddy, mais ça évite de prendre le navigateur de quelqu'un d'autre pour lancer une attaque.

ON va faire un exemple.

Imagine que sur le site de ta banque il y a ce script php:

<?php
if (isset($_POST['send'], $_POST['dest'], $_POST['amount'])) {
// ... traitement ...
die("Votre versement de {$_POST['amount']}€ en faveur du compte destinataire {$_POST['dest']} a bien été exécuté.");
}
?>
<html>
<form action="" method="post">
<p><label for="dest">Compte destinataire: </label><input type="text" id="dest" name="dest" /></p>
<p><label for="amount">Montant: </label><input type="text" id="amount" name="amount" /></p>
<p><input type="submit" name="send" value="Valider" /></p>
</form>
</html>


Maintenant, imagine qu'un voleur a trouvé un moyen d'injecter ce bout de javascript sur sa page facebook :

var form = document.createElement('form');
var amount = document.createElement('input'), dest = document.createElement('input'), send = document.createElement('input');
form.setAttribute('action', 'http://tabanque.com/envoyer-argent.php');
form.setAttribute('method', 'post');
amount.setAttribute('name', 'amount');
amount.setAttribute('value', 100);
dest.setAttribute('name', 'dest');
dest.setAttribute('value', 'Le_compte_bancaire_du_voleur');
send.setAttribute('type', 'submit');
send.setAttribute('name', 'send');
send.setAttribute('value', 'Valider');
form.appendChild(dest);
form.appendChild(amount);
form.appendChild(send);
form.submit();


Maintenant, si par hasard tu es loggé et chez ta banque et chez facebook, et que tu vas voir la page du voleur, paf ! Merci pour les 100€ !

Ca suppose bien sûr qu'il existe une faille chez facebook. La faille de facebook dans cet exemple est un XSS, c'est-à-dire qu'il y a un moyen d'injecter un code javascript non désiré par facebook. Du côté de la banque, la faille est un CSRF: l'attaquant s'est servi d'un autre site ou d'une autre personne pour faire son attaque sur le site cible.

Hélas, énormément de sites sont vulnérables, autant aux XSS qu'aux CSRF, beaucoup plus qu'on ne le croit.

Si la banque avait mis un token dans son formulaire, il aurait fallu que l'attaquant le récupère d'abord avant de pouvoir correctement envoyé son virement. Pour le faire, ça nécéssite soit d'afficher une iframe, soit de faire de l'AJAX, mais heureusement le cross-domain AJAX de même que la communication de code entre deux iframes de deux domaines différents est interdite (sauf autorisation explicite des deux parties impliquées).
Modifié par QuentinC (14 Nov 2013 - 16:11)
Je comprends mieux. Je suppose donc qu'en théorie, les liens internes aussi, et contenant des valeurs passées en GET, devraient également être assortis d'un token ?
Peut-on considérer que si le lien ne sert qu'à afficher des informations sur une page (par exemple, les renseignements sur un utilisateur) alors le token est inutile ?
Fix a écrit :
Je comprends mieux. Je suppose donc qu'en théorie, les liens internes aussi, et contenant des valeurs passées en GET, devraient également être assortis d'un token ?
Peut-on considérer que si le lien ne sert qu'à afficher des informations sur une page (par exemple, les renseignements sur un utilisateur) alors le token est inutile ?


Oui aux deux, tu supposes bien. Smiley smile

Token sur tous les liens qui effectuent une action (vote, suppression, etc).
Modifié par jb_gfx (14 Nov 2013 - 16:09)
Merci à vous deux. Ça me semble clair maintenant.

Je viens de voir que QuentinC avait édité sa réponse pour y ajouter un exemple. Merci beaucoup !
Je rencontre quelques difficultés dans la mise en place de mes jetons de sécurité.

J'ai voulu suivre les conseils donnés sur SO ici (dans la première réponse), notamment cette partie :
a écrit :
Combine it with a redirect so you keep a perfect backward and forward behavior. See the POST / redirect / GET pattern for more information about the redirect.

Du coup, j'utilise le code suivant pour la vérification de mon jeton :
if (!isset($_SESSION["token_update_email"]) || !isset($_POST["token_update_email"]) || $_SESSION["token_update_email"] != $_GET["token_update_email"]) {
	header("Location: index.php"); exit();
}
unset($_SESSION["token_update_email"]);
// Mise à jour de la BDD

Résultat : si je clique deux fois sur un lien, je me retrouve sur la page d'accueil...
Cette partie sur la redirection a-t-elle un intérêt réel ? Si oui, comment m'en sortir ?

Merci de votre aide !
a écrit :
Cette partie sur la redirection a-t-elle un intérêt réel ?

Oui, mais je dirais que c'est plutôt pour le confort d'utilisation que pour la sécurité. Ca permet d'assurer que les boutons actualiser/suivant/précédent se comportent correctement, et ça c'est important pour avoir un système utilisable et accessible (je trouve très désagréable les sites où ces boutons n'ont pas un comportement logique). Par contre du point de vue sécurité, la redirection n'apporte pas grand chose du moment que tu effaces bien le token une fois qu'il a été consommé.
Je ne vois pas de problème avec ton code à priori.

Par contre il faut que ça soit le plus transparent possible. Donc si tu reçois un token invalide, il ne faudrait pas rediriger sur index.php mais sur la page du formulaire, sans prendre en compte les modifs bien sûr.
QuentinC a écrit :
il ne faudrait pas rediriger sur index.php mais sur la page du formulaire, sans prendre en compte les modifs bien sûr

Donc sans rediriger et sans rien changer, ça doit fonctionner aussi ? On réaffiche juste le formulaire, sans avoir tenu compte des modifs ?
Dans ce cas, je ne comprends pas la réponse donnée sur SO et la citation que j'en ai faite dans mon message #16...
Perso comme je comprends la chose, il faut simplement faire en sorte qu'à aucun moment on arrive dans le cas où le navigateur doit demander à l'utilisateur s'il faut ou non renvoyer le formulaire. La fameuse boîte de dialogue pose problème parce que soit on répond oui et ça renvoie le formulaire, conduisant à des bugs potentiels sur le serveur, soit on répond non et c'est le navigateur qui fait n'importe quoi du genre afficher une page type erreur/introuvable (IE!), ou tout au moins il affiche une page qui n'est plus à jour.

Pour que cette boîte de dialogue n'arrive jamais, il faut s'assurer que tu fais toujours une redirection GET à chaque fois que tu reçois une requête POST, que les données reçues soient prises en compte ou non au final. Ca peut être une redirection sur la même page, notamment s'il y a une erreur, que ce soit une erreur de sécurité ou simplement de saisie quelconque d'ailleurs.

En fait, comme je l'ai déjà dit, ça n'a pas grand chose à voir avec la sécurité, c'est plutôt pour le confort d'utilisation: on évite tant que possible que les données puissent être soumises deux fois, si tout va bien on passe à la suite et s'il y a une erreur, on revient au formulaire erronné. La sécurité là-dedans, elle est déjà assurée par ton token: s'il est bien conçu, la double soumission n'est pas possible de toute façon.
Merci de ta réponse de nouveau très détaillée.

QuentinC a écrit :
Pour que cette boîte de dialogue n'arrive jamais, il faut s'assurer que tu fais toujours une redirection GET à chaque fois que tu reçois une requête POST, que les données reçues soient prises en compte ou non au final.

À une époque où l'on trouve l'AJAX prôné partout pour éviter le rechargement des pages, c'est un peu contraignant, non ?
Je me trouve par exemple confronté à la réalisation d'un formulaire en plusieurs étapes. Quand une première étape est validée, j'affiche la deuxième à compléter, et ainsi de suite. Si je fais une redirection à chaque fois (ce n'est pas le cas pour l'instant, toutes les étapes étant inscrites dans le même fichier php), est-ce que ça ne donne pas l'impression au visiteur d'un temps de chargement long, et de redirections multiples ? (puisque une première fois la page est rechargée lors de l'envoi de la requête POST, et une seconde fois, si cette requête est validée, on redirige vers la même page)
a écrit :
À une époque où l'on trouve l'AJAX prôné partout pour éviter le rechargement des pages, c'est un peu contraignant, non ?

Tu n'as qu'à pas utiliser AJAX ! En plus ça t'éviteras de nombreux tracas en matière d'accessibilité, de référencement et j'en passe.

Le quasi 100% des sites web n'ont pas compris comment utiliser AJAX correctement et de manière accessible pour que ce soit agréable à utiliser et non contraignant en ce qui concerne la navigation au clavier ou avec des aides techniques. Avec des formulaires classiques sans AJAX il y a beaucoup moins ce problème. Je ne suis pas contre AJAX, mais seulement pour une utilisation rationnelle. On n'est pas obligé d'en mettre partout. En l'occurence après un gros formulaire, à mon avis, ça sert à rien.

De toute façon, si tu fais de l'AJAX, tu t'en fous des redirections: ton formulaire n'est jamais réellement soumis du point de vue du navigateur puisque tout est géré en javascript, donc tu n'as pas de risques de double envoi. Et si ça arrive, c'est que ton script est mal foutu.

Là où le serpent se mord la queue, c'est que, en ce qui concerne l'accessibilité, on recommande souvent de faire une version sans AJAX au cas où javascript est désactivé. La première grosse erreur c'est de faire l'amalgamme entre pas de souris / utilisateur clavier / lecteur d'écran et pas de javascript, mais c'est un autre débat. La deuxième c'est que vu que tu as prévu ton truc principalement en AJAX et que donc tu n'as pas mis de redirections parce qu'avec AJAX ça n'a aucun intérêt, c'est dans ce mode sans AJAX que la boîte de dialogue peut revenir. Donc tu devrais mettre des redirections pour que ce cas soit géré correctement, aussi rarement puisse-t-il être invoqué.

Après, ben, si AJAX est trop con pour tenir compte des redirections 302, c'est pas ma faute, il faut taper Microsoft et/ou Apple et/ou Google et/ou Mozilla, et en attendant, ne pas utiliser AJAX, ou utiliser des URL différentes en mode AJAX et en mode non-AJAX ou se donner le moyen de les différencier. Je n'ai pas essayé pour voir si les redirections 302 étaient bien gérées par AJAX, mais en fait je ne pense pas.
Modifié par QuentinC (21 Nov 2013 - 08:31)
Je n'ai jamais dit que j'utilisais de l'AJAX (ça dépasse d'ailleurs largement mes compétences). Je m'interrogeais seulement sur la réaction d'un utilisateur, habitué à un traitement rapide et sans rechargement de page des formulaires, qui tomberait sur mon site où le formulaire est soumis, puis où il est redirigé, avant de pouvoir saisir la suite.

Ceci étant, ça ne change rien à l'intérêt de ta réponse Smiley cligne Je vais en rester là concernant mes formulaires : un jeton bien placé, supprimé dès lors que le formulaire est soumis, et pas de javaScript pour compliquer tout ça.

Merci encore pour toute l'aide et les réflexions apportées.
Bonne soirée à tous !
Pages :