Nous avons vu que le navigateur utilise 2 méthodes privilégiées pour faire une requête au serveur : POST et GET. Ces 2 méthodes permettent de passer des informations au serveur, soit dans le corps du message HTTP (POST), soit à la suite de l'url demandée (GET). Nous allons voir ici plus en détail le passage d'informations par url et leur récupèration dans le tableau $_GET.

Passer des paramètres dans un lien

Principes

Une url est composée des parties suivantes :

composition d'une URL

C'est la partie "query string" qui nous intéresse. Les informations sont passées sous la forme de couples nom=valeur, séparés par le caractère &. Sur le serveur, avec PHP, on retrouve ces couples dans le tableau $_GET : le nom devient une clé et la valeur est la valeur associée à cette clé.

Exemple de lien :
<a href="lien01_get.php?id=12345&nom=piat" target="_blank">cliquez ici</a>
Rendu :
cliquez ici

L'attribut target permet de spécifier la cible dans laquelle va s'afficher le lien (ici un nouvel onglet pour qu'on puisse voir l'url dans la barre d'url du navigateur)

Le code PHP du fichier lien01_get.php est le suivant :

<?php
require('bib_fonctions.php');

htmlDebut('Informations transmises dans une url');

foreach ($_GET as $nom => $valeur) {
	htmlInfo($nom);
	echo $valeur;
}

htmlFin();
?>

Protection des chaînes

Si les valeurs des paramètres passés dans l'url sont des chaînes de caractères, elles doivent être protégées car les urls ont un codage spécifiques pour certains caractères (caractères de séparations, caractères accentués, etc). On utilise :

  • urlencode() du côté de l'émetteur, pour protéger la chaîne avant de la placer dans l'url,
  • du côté du récepteur, on n'a pas besoin de décoder la chaîne pour retrouver la chaîne originale car PHP le fait automatiquement pour tout ce qu'il stocke dans $_GET.
Exemple : paramètres dans des liens

Dangers

Le lien de l'exemple s'ouvrira dans un nouvel onglet pour que vous puissiez voir/manipuler l'url.

Remarquez l'encodage réalisé par urlencode() :
- le caractère ç (codé avec le charset ISO-8859-1) est remplacé par %E7
- le caractère = est remplacé par %3D
- le caractère espace ( ) est remplacé par un +
- les caractères alphanumériques ne sont pas modifiés

On utilise fréquemment les paramètres dans les urls pour passer des informations de page en page bien que l'une des faiblesses viennnent de la modification manuelle possible de l'url affichée par le navigateur.

On peut ainsi facilement passer des informations qui n'étaient pas attendues.

  • Modifiez par exemple mon nom puis validez. Le script est exécuté à nouveau, et la valeur modifiée du nom est affichée.
  • On peut de la même façon modifier le nom d'un couple nom=valeur ou en ajouter un et redemander la page.

Le passage d'informations par les urls doit donc être entouré de beaucoup de précautions :

  • si le nombre de couples nom=valeur est différent de celui attendu, le script doit être arrêté et tous les éléments éventuellement rattachés à l'utilisateur (session par exemple) doivent être supprimés,
  • les noms des couples reçus doivent être validés,
  • les valeurs des couples reçues doivent être validées.

En l'état actuel, lors de la réception des données, il faudrait, dans ce script, protéger les chaînes provenant de la "query string" qui sont envoyées au navigateur avec la fonction htmlentities()

Cryptage des paramètres

Le cryptage et la signature des données permettent de cacher et de protéger les valeurs passées dans les urls.

Le cryptage de données n'est pas la même chose que la signature de données (faite avec par exemple la méthode md5 bien connue) :

La cryptographie et l'étude des différents modes de cryptage dépassent de loin le cadre de ce tutoriel et nous n'entrerons pas dans les détails de tel ou tel algorithme.

Crypter / décrypter

Pour réaliser des cryptages selon différentes méthodes et algorithmes, nous allons utiliser l'extension openSSL de PHP qui définit la fonction openssl_encrypt() et son pendant openssl_decrypt(). Généralement cette extension est compilée avec PHP, mais on peut s'en assurer avec la fonction function_exists() très pratique pour savoir ce qui est disponible dans la configuration ou version de PHP.

Exemple : vérification existence des fonctions de cryptage

Pour crypter/décrypter des données on peut donc utiliser ces fonctions openssl_encrypt() et openssl_decrypt() comme dans l'exemple suivant qui utilise l'algorithme AES (Advanced Encryption Standard) en mode CBC.

Exemple : cryptage / décryptage

Remarques :

  • Pour crypter $val, la fonction openssl_encrypt() utilise un algorithme (le paramètre $cipher), une clé de cryptage $cle, et un vecteur d'initialisation nommé $iv, c'est à dire une chaîne pseudo-aléatoire d'octets, dont la longueur $ivlen dépend de l'algorithme utilisé.
  • Avec l'option OPENSSL_RAW_DATA, la fonction openssl_encrypt() renvoie une chaîne cryptée binaire; cela signifie qu'elle contient des octets qui peuvent prendre n'importe quelle valeur.
  • Pour retrouver la chaîne originale, la fonction openssl_decrypt() a besoin de l'algorithme utilisé, de la chaîne cryptée, de la clé et du vecteur d'initialisation.
  • A chaque fois qu'on crypte une nouvelle donnée, il faut générer un nouveau vecteur d'initialisation.
  • Dans une "vraie" application Web, il ne faut pas envoyer directement au navigateur une chaîne binaire pouvant contenir n'importe quel octet. Si vous testez le code HTML généré avec le validateur W3C, il y détectera des caractères interdits. De même, la chaîne binaire pourrait contenir des caractères '<' et '>' ayant une signification particulière en HTML. Nous le faisons ici uniquement dans un but pédagogique.
  • Le nombre 128 dans le nom de l'algorithme de cryptage AES-128-CBC indique la longueur de la clé de cryptage en nombre de bits qu'il faut utiliser avec cet algorithme :
    - 128 bits = 16 * 8 bits = 16 octets
    - si on utilise une clé plus longue, elle sera tronquée et seuls les 128 premiers bits seront utilisés pour crypter les données.

Pour générer une clé de cryptage "correcte" à utiliser avec l'algorithme AES-128-CBC, on peut utiliser le script suivant :

Exemple : génération d'une clé de cryptage

La fonction openssl_random_pseudo_bytes() renvoie une chaine binaire qu'il ne faut donc pas afficher telle quelle dans le navigateur. La fonction base64_encode() permet d'encoder cette chaine binaire en Base64, ce qui permet d'obtenir une chaîne ASCII qui la représente, qu'on peut envoyer sans problème au navigateur pour pouvoir la copier/coller dans le script qui suit.

Ré-écriture de l'exemple sur le passage de paramètres dans une url :

Exemple : paramètres cryptés dans des liens

Le lien de l'exemple s'ouvrira dans un nouvel onglet pour que vous puissiez voir/manipuler l'url.

La longueur de la chaîne cryptée n'est pas constante.

La clé de cryptage est définie par une constante. Par conséquent, pour être capable de décrypter la chaîne cryptée à la réception des paramètres, il faut transmettre dans l'url la chaîne cryptée (retournée par openssl_encrypt()) et le vecteur d'initialisation $iv. Comme le vecteur d'initialisation $iv a toujours la même longueur, nous pouvons les concaténer : on sera capable de séparer les 2 chaines lors de la réception. Ces 2 chaînes sont des chaînes binaires. Il est donc absolument nécessaire de les encoder avec urlencode() si on veut les passer dans une url.

La fonction urlencode() remplace tous les caractères non alphanumériques (hormis -_.) par des séquences commençant par un caractère pourcentage (%), suivi de deux chiffres hexadécimaux. Les espaces sont remplacés par des signes plus (+). Cela signifie que tous les octets qui ne sont pas des codes ASCII des caractères alphanumériques, ou du caractère (-), ou du caractère (_), ou du caractère (.) ou du caractère espace ( ) sont remplacés par 3 octets. Donc, quand on lui fournit une chaîne binaire pseudo-aléatoire, la fonction urlencode() remplace (256 - (26*2 + 10 + 4))/256 ≈ 75% des octets par 3 octets (les 25% restant ne sont pas modifiés, sauf l'espace qui est transformé en signe plus (+)).
Conclusion : l'encodage d'une chaine binaire pseudo-aléatoire de longueur L avec urlencode() donne, en moyenne, une chaîne ASCII de longueur
(L/4 + 3*3L/4) = 2,5L.

Décommentez maintenant les appels des fonctions base64_encode() et base64_decode() dans l'exemple précédent, et vous constaterez que l'appel de base64_encode() avant celui de urlencode() permet de réduire dans des proportions importantes la longueur de la "query string". En effet, le passage de la chaine à transmettre par la fonction base64_encode() multiplie sa longueur approximativement par 4/3=1,33 (24 bits dans la chaine d'origine, soit 3 octets, sont codés sur 4 octets dans la chaîne encodée en Base64). Mais comme le codage en Base64 utilise 65 caractères dont 62 sont alphanumériques, après passage par la fonction urlencode(), on obtient au final une chaîne encodée url de longueur à peine supérieure à la longueur de la chaine encodée en Base64.

Signer le cryptage

Si nous nous arrêtons au point précédent, certes les données transmises dans la "query string" sont cryptées donc inintelligibles, mais nous avons un problème : rien ne garantit que la donnée reçue et décryptée est bien la même que celle qui a été cryptée et envoyée. On peut toujours modifier manuellement l'url et on obtiendra toujours quelque chose au décryptage, même si ce quelque chose est totalement incohérent. Dans le cas d'un nombre attendu, on pourra, si on reçoit une chaîne non numérique, se douter que l'url a été modifiée, mais dans le cas d'une chaîne de caractères attendue, on aura plus de mal à définir si on est bien en face d'une tentative de corruption.
Pour vous en convaincre, essayez, dans l'exemple précédent, de remplacer un caractère par un autre dans la valeur d'un couple nom=valeur, avec ou sans appel des fonctions base64_encode() et base64_decode().

C'est ici que la signature de données intervient. Avec de tels algorithmes ("md5", "sha256", "haval160,4", "crc32", etc) nous allons pouvoir vérifier si les données reçues sont bien égales aux données émises. Pour cela :

  • on calcule la signature de la chaine cryptée émise;
  • on transmet cette signature dans l'url;
  • à la réception, on calcule la signature de la chaine cryptée reçue;
  • on compare la signature reçue avec la signature calculée à la réception;
  • si les 2 signatures sont égales, on considère que la chaine cryptée émise est égale à la chaine cryptée reçue; sinon, on est sûr que la "query string" a été modifiée.

Nous utiliserons la fonction hash_hmac() pour signer nos paramètres. On doit fournir à cette fonction le nom de l'algorithme à utiliser. Le choix est très grand : voir la documentation de la fonction hash_hmac_algos() pour une liste des algorithmes disponibles. Dans l'exemple suivant, nous utiliserons l'algorithme de hachage 'sha256' qui renvoie une signature de 32 octets (soit 256 bits) de long.

Pour utiliser l'algorithme de hachage 'sha256', nous devons générer une clé de 32 octets :

Exemple : génération des clés de cryptage et de hachage

Un algorithme de hachage renvoie toujours une signature de la même longueur, quelque soit la longueur de la chaîne de caractères qui est signée.
Par exemple sha1 renverra une signature de 40 octets, sha224 de 56 octets, sha512 de 128 octets, md5 de 32 octets, etc.

Comme la signature est de longueur fixe, nous pouvons également la concaténer avec la chaîne cryptée et le vecteur d'initialisation $iv, et transmettre le tout dans la valeur d'un couple nom=valeur (nous choisissons dans l'exemple ci-dessous de transmettre le vecteur d'initialisation suivi de la chaine cryptée au milieu de la signature de la chaine cryptée pour "brouiller les pistes").

Exemple : paramètres cryptés et signés dans des liens

Le lien de l'exemple s'ouvrira dans un nouvel onglet pour que vous puissiez voir/manipuler l'url.

La fonction hash_equals() compare deux chaînes en utilisant toujours le même temps de calcul, qu'elles soient égales ou pas. Elle est utilisée pour ne pas donner d'indications "temporelles" à d'éventuels hackers.

Quand PHP compare 2 chaînes avec l'opérateur ==, le temps de calcul dépend de la position du premier caractère différent dans les 2 chaines; plus le nombre de caractères identiques placés au début des 2 chaines augmente, plus la durée de la comparaison augmente.

Pour éviter d'avoir des urls trop longues et avec trop d'éléments et de calcul de cryptage / signature à faire, on peut concaténer les valeurs des paramètres avec un caractère séparateur comme dans l'exemple suivant.

Exemple : paramètres cryptés et signés dans des liens

Le lien de l'exemple s'ouvrira dans un nouvel onglet pour que vous puissiez voir/manipuler l'url.

La version 7.1 de PHP a ajouté des paramètres aux fonctions openssl_encrypt() et openssl_decrypt() permettant d'authentifier la chaine cryptée lors du décryptage.
La fonction openssl_encrypt() renvoie toujours la chaine cryptée, et on récupère, dans la chaîne $tag transmise par référence, une chaine permettant d'authentifier la chaine cryptée reçue. Notez bien que la fonction openssl_encrypt() écrit dans $tag.
Cette chaîne $tag doit ensuite être transmise à la fonction openssl_decrypt() qui va la lire afin d'authentifier la chaîne cryptée reçue.

Pour pouvoir utiliser ces paramètres supplémentaires et donc le paramètre supplémentaire $tag, il faut utiliser un algorithme en mode AEAD (GCM ou CCM).

Dans l'exemple suivant, on utilise toujours l'algorithme AES-128, mais cette fois-ci en mode GCM : il crypte ET signe les données. La fonction openssl_encrypt() permet de fixer la longueur du tag d'authentification. Sa valeur peut être entre 4 et 16 pour le mode GCM. Dans l'exemple suivant, on utilise la valeur par défaut du paramètre correspondant : le tag d'authentification a une longueur fixe égale à 16 octets.

Exemple : paramètres cryptés et signés dans des liens

Le lien de l'exemple s'ouvrira dans un nouvel onglet pour que vous puissiez voir/manipuler l'url.