Post by mpgDans une discussion voisine a été évoquée la méthode des jetons à usage
unique pour contribuer à sécuriser certains aspects sensibles, comme la
modification/suppression de données.
Avez-vous des liens vers des exemples clairs de mise en œuvre correcte
cette méthode en PHP, et si possible explicant les principaux cas où il
est opportun de l'utiliser ? Ou une bonne âme se sent-elle d'humeur à me
fournir ici même l'exemple et/ou les explications ?
Pas de lien sous la main, d'autres compléteront, je me permets juste
quelques pistes.
L'usage des jetons uniques permet notamment de résoudre les problèmes
décrits dans certaines règles de
http://innocentcode.thathost.com/rules.html
(le site du bouquin dont j'ai parlé dans le même fil où l'on parlait des
jetons récemment)
que je traduis rapidement :
Règle 3: dans le contexte du serveur, la notion de sécurité côté client
n'existe pas
Règle 19: quand cela est possible, utiliser une couche d'indirection pour
les données générées par le serveur
Règle 20: passer aussi peu d'informations que possible sur l'état interne
du serveur au client
Règle 21: ne pas prendre pour acquis que les requêtes viendront dans un
certain ordre
Cela tourne autour des problèmes de modification (y compris suppression)
de données, typiquement dans une base de données côté serveur (mais peu
importe comment les données sont stockées), et avec différents degrès
d'accès (tout n'est pas modifiable par tout le monde, et il y a
potentiellement une authentification préalable).
On peut aussi de la même façon tenir compte des cas de formulaires
tronçonnés en plusieurs pages quelque soit la raison.
Dans tous ces cas, le serveur doit exposer certains détails (ex: numéro
de l'enregistrement en cours de modification) au client, qui certes a
priori n'en fera rien, mais va les renvoyer ultérieurement, c'est
l'exemple des champs cachés.
S'il y a plusieurs pages (y compris lors de la suppression genre une page
de confirmation, ou une page avec une liste d'enregistrement à supprimer
et une suivante pour la modification, etc.) on veut que le client passe
bien dans l'ordre qu'on a prévu. Conformément à la règle 21, cela ne peut
être garanti, ce qui expose d'ailleurs au cas des liens malicieux dans
les emails etc... dont la simple lecture (voire même pas avec une URL
dans un img src et le visionnage automatique des images côté client, même
pas d'action de l'utilisateur...) provoque une action, en "sautant" des
étapes.
Pour forcer cela, il faut donc trouver un moyen. Tester le Referer ne
marche pas (il peut être absent, et trafiquable aisément de toute façon).
Alors l'idée est de mettre une information dans la page 1, et on ne
donnera pas la page 2 si on (le serveur) ne reçoit pas cette information,
compte-tenu que ce "jeton" (token) est tel qu'il ne peut être deviné par
le client (on va donc éviter le compteur qui s'incrémente... et préférer
un hachage style SHA1 avec une partie fixe - un "mot de passe" - et une
partie variable - comme l'heure).
Donc en résumé, au début, le serveur génère un jeton unique, c'est une
chaine opaque, sans aucun sens, qui sera stocké par le client dans le
formulaire HTML ou l'URL (GET vs POST :-))
Il vaut mieux ne pas utiliser de cookie pour cela, ou lier cette
information à la session en cours (s'il y en a une, et sauf si la session
est gérée sans cookies, ce qui est rare), parce que on veut pouvoir
permettre deux onglets par exemple, sur deux formulaires du même serveur
mais des enregistrements différents (donc des jetons différents) en même
temps ou presque.
Lorsque le client demande la "page 2", le serveur peut donc vérifier s'il
a le bon jeton (car il aura stocké ce jeton au moment de son envoie lors
de la page 1, et ce jeton sera lié à la session en cours - s'il y en a
une - pour éviter que les clients puissent échanger les jetons ; et il
faudra aussi un mécanisme d'expiration pour supprimer automatiquement les
jetons côté serveur au bout d'un certain temps de non utilisation, comme
les identifiants de session en fait), et le client n'aura la page 2 (ou
la réalisation de son action de suppression) que s'il a fourni le bon
jeton.
Sinon on pourra estimer que c'est une attaque, ou que le client ne suit
pas le bon chemin, volontairement ou non (ca peut être un bug de
l'application aussi).
Une fois qu'on commence à passer un jeton unique dans chaque formulaire,
on peut aussi arriver à la conclusion, en phase avec les règles 19 et 20
citées précédemment, qu'on n'a en fait plus trop besoin de passer quoi
que ce soit d'autre dans les formulaires :-)
Ce qui résout un autre problème. Mettons qu'on soit en cours d'édition de
l'enregistrement numéro 42 de la base. Cette information sera quelque
part côté client, dans un champ caché du formulaire ou dans l'URL.
C'est gênant car cela donne des indications au client sur le stockage
côté serveur. Il ne faut pas être très curieux pour se dire que ca
pourrait être rigolo de changer le code et mettre 43 au lieu de 42 et
voir ce qui se passe.
Et là on voit de tout... Soit on peut ainsi récupérer toute la base de
proche en proche, voire tout supprimer (selon ce qu'on fait), etc. parce
que le serveur n'aura jamais imaginé que le client donne autre chose que
ce qu'il est sensé donné (règle 3). Même si la phase d'autorisation est
correctement gérée on pourra avoir un message "accès refusé" qui est
différent de "enregistrement inexistant", parfois dans une attaque, la
simple connaissance que quelque chose existe même sans pouvoir y accéder
à ce stade, est utile.
Donc si on cumule tout ce qui précède on fait cela :
- le client veut modifier/supprimer l'élément 42 (qu'il aura sélectionné
via un lien, une liste déroulante, etc.)
- le serveur génére un jeton unique, il le stocke, le lie à la session,
et lie aussi ce que j'appelle un "form bag" c'est à dire un lot de
données, ici, il stockera id=42 pour
- le serveur renvoit au client donc un code HTML avec tout ce qu'il faut
pour modifier ce qu'il faut, et une seule chose en champ caché : le
numéro du jeton
- quand le client soumet la page, le serveur récupère le jeton, il
vérifie (dans cet ordre) qu'il est valide (syntaxiquement), non expiré,
correctement lié à la session en cours, puis récupère les données
associées (le form bag stocké uniquement côté serveur) et sait ainsi
qu'il faut opérer sur l'enregistrement 42.
Mais ainsi le client ne peut pas essayer de "taper" ailleurs, car il n'a
rien à modifier, juste le jeton. Si on assure que le jeton est opaque,
unique et non généré de manière triviale, alors le client est obligé de
prendre le chemin que le serveur lui donne, tout le reste sera détecté et
ineffectif.
On peut gagner aussi légérement en performances (moins de données à
échanger), et c'est applicable à AJAX.
Dis autrement (parfois c'est utile de dire les choses autrement avec un
autre point de vue), le jeton unique et la valise de données associée est
une forme de sérialisation de l'état de l'application à un instant t
pour un client donné (juste avant son départ sur le réseau vers le
navigateur), pour pouvoir le récupérer à un instant t' (juste après le
retour en provenance du navigateur)
Cela nécessite cependant de voir toute l'application un peu différemment,
et c'est un peu plus lourd à mettre en place, sans être impossible. En
contre-partie on déjoue tout de même un certain nombre de problèmes (en
en introduisant d'autres, par exemple autour de la génération, du
stockage et de la durée de vie des jetons)
Bon je ne sais pas si je suis très clair finalement.
Par contre rien de ce qui précède n'est spécifique à PHP. C'est une bonne
pratique des applications sans état (donc du web), en général.
--
Patrick Mevzek . . . . . . . . . . . . . . Dot and Co
<http://www.dotandco.net/> <http://www.dotandco.com/>
<http://www.dotandco.net/ressources/icann_registrars/prices>
<http://icann-registrars-life.dotandco.net/>