Encodage, hachage et chiffrement sont trois choses différentes
Une chaîne qui a l'air brouillée n'est pas une chaîne protégée. Trois opérations distinctes produisent une sortie illisible, et c'est en les confondant que naissent de vraies failles de sécurité.
| Réversible ? | Nécessite une clé ? | À quoi cela sert | |
|---|---|---|---|
| Encodage | Oui, par n'importe qui | Non | Faire passer des octets par un canal qui ne peut pas les transporter bruts |
| Hachage | Non | Non | Empreintes, contrôles d'intégrité, clés de recherche |
| Chiffrement | Oui, avec la clé | Oui | Le secret |
L'encodage est un changement d'alphabet. Base64, l'encodage pourcent, l'hexadécimal et les entités HTML écrivent la même information autrement, pour qu'un analyseur en aval ne s'étrangle pas dessus. Aucun secret n'entre en jeu, il n'y a donc rien à garder. Un décalage de César relève de la même catégorie : l'outil Chiffre de César essaie les 25 décalages d'un coup par force brute, ce qui résume assez bien la protection qu'offre une substitution fixe. Le hachage projette n'importe quelle entrée sur une empreinte de longueur fixe et ne peut pas être exécuté à l'envers. Le chiffrement est le seul des trois à fournir de la confidentialité, et il fait toujours intervenir une clé. Si vous ne pouvez pas désigner la clé, rien n'est chiffré.
Base64 et ce qu'il coûte
Base64 découpe trois octets (24 bits) en quatre groupes de six et écrit chaque groupe sous la forme d'un caractère tiré d'un alphabet de 64 caractères. Quatre caractères pour trois octets signifient une sortie plus volumineuse d'un tiers. Cela compte surtout pour l'intégration de fichiers : l'outil Fichier vers Base64 produit une URL de données, si bien qu'une image de 300 Ko atterrit dans votre HTML à environ 400 Ko, retéléchargés à chaque page.
Quand l'entrée n'est pas un multiple de trois octets, le dernier groupe est complété par des = :
"abc" -> YWJj (3 bytes, no padding)
"ab" -> YWI= (2 bytes, one =)
"a" -> YQ== (1 byte, two =)
L'alphabet standard se termine par + et /, ce qui pose problème dans une URL, car + se décode en espace dans des données de formulaire et / est un séparateur de chemin. Un second alphabet existe donc : Base64url remplace + par - et / par _, et abandonne en général le remplissage. C'est pourquoi un jeton copié depuis une URL échoue parfois dans un décodeur réglé sur l'alphabet standard. L'Encodeur Base64 gère les deux, et gère correctement l'Unicode, ce que les implémentations naïves ne font souvent pas : le texte doit être encodé en UTF-8 avant l'étape Base64.
Rien de tout cela n'est une mesure de sécurité. Une valeur qu'un produit appelle un identifiant « chiffré » dans une URL est très souvent du Base64 d'un simple entier que n'importe qui peut lire, modifier et réencoder en quelques secondes. Il en va de même pour la charge utile d'un JSON Web Token : le Décodeur JWT la lit sans clé, parce qu'il n'y a rien à déverrouiller.
L'encodage pourcent, et quelle fonction employer
L'encodage pourcent remplace un octet par % et deux chiffres hexadécimaux. Il travaille sur des octets et non sur des caractères : un texte non ASCII est donc d'abord encodé en UTF-8, et é devient %C3%A9.
Les caractères non réservés (A-Z, a-z, 0-9, -, ., _, ~) n'ont jamais besoin d'être encodés. Les caractères réservés (: / ? # [ ] @ ! $ & ' ( ) * + , ; =) sont les délimiteurs qui donnent sa structure à une URL, et savoir si l'un d'eux doit être encodé dépend de son emploi comme délimiteur ou comme donnée. C'est là toute la différence entre les deux fonctions JavaScript :
encodeURI("https://ex.com/s?q=cats & dogs")
// https://ex.com/s?q=cats%20&%20dogs the & still splits the query
encodeURIComponent("cats & dogs")
// cats%20%26%20dogs safe as a single value
encodeURI s'applique à une URL entière que vous voulez rendre légale : elle laisse donc les délimiteurs intacts. encodeURIComponent s'applique à une seule valeur destinée à un segment de chemin ou à un paramètre de requête : elle les encode donc. Employez la seconde pour tout ce que vous insérez, et notez qu'elle laisse encore !, ', (, ) et * tranquilles.
Reste le signe plus. Les corps de formulaire envoyés en application/x-www-form-urlencoded encodent un espace par +, et la convention a débordé sur les chaînes de requête : la plupart des serveurs décodent donc un + dans une requête comme un espace. Un plus littéral doit être envoyé sous la forme %2B, ce qui explique que alex+billing@example.com arrive si souvent sous la forme alex billing@example.com, l'étiquette détruite. Dans un segment de chemin, en revanche, + n'est qu'un plus et un espace s'écrit %20. L'Encodeur d'URL rend cela visible quand une valeur survit à un aller-retour et se casse au suivant.
L'échappement dépend de l'endroit où atterrit la valeur
Il n'existe pas de forme unique « échappée pour le web » d'une chaîne. C'est le contexte qui décide.
| Contexte | Traitement correct |
|---|---|
| Texte HTML | Des entités pour &, < et > |
| Attribut entre guillemets | Des entités pour & et pour le guillemet qui l'entoure, et toujours entourer l'attribut de guillemets |
À l'intérieur de <script> | L'échappement de chaîne JavaScript, pas des entités |
URL dans href ou src | Encoder la valeur en pourcent, l'échapper comme attribut, puis vérifier le schéma |
| Valeur CSS | L'échappement CSS, et ne jamais construire url() à partir d'une saisie utilisateur |
Le cas du script prend les gens au dépourvu. Le contenu d'un script ne subit pas de décodage d'entités : une entité écrite là reste littérale, et l'analyseur HTML ferme le bloc sur la séquence </script où qu'elle apparaisse, entre guillemets ou non :
<script>var s = "</script>";</script> <!-- block ends early -->
<script>var s = "<\/script>";</script> <!-- correct -->
L'échappement seul ne suffit pas non plus pour les URL. Une valeur commençant par javascript: s'exécute au clic quelle que soit la minutie de son encodage : validez donc le schéma face à une liste autorisée. L'outil Entités HTML couvre les cas du texte et des attributs et redécode les entités, ce dont on a habituellement besoin quand une API a doublement échappé quelque chose en &amp;.
Hachage : quel algorithme, et pour quoi
| Algorithme | Empreinte | État |
|---|---|---|
| MD5 | 128 bits | Cassé. Les collisions sont triviales |
| SHA-1 | 160 bits | Cassé. Les collisions à préfixe choisi sont réalisables et abordables |
| SHA-256, SHA-512 | 256, 512 bits | Convient pour l'intégrité et les signatures |
| bcrypt, scrypt, Argon2 | variable | Mots de passe uniquement |
MD5 et SHA-1 échouent sur la résistance aux collisions : un attaquant peut construire deux entrées différentes ayant la même empreinte. Partout où un hachage tient lieu de document, ils sont inutilisables, qu'il s'agisse de signatures, de certificats, d'adressage par contenu ou de déduplication de ce qu'un attaquant fournit. Aucun des deux n'est cassé pour les préimages, si bien qu'une vieille somme de contrôle MD5 détecte encore une corruption accidentelle, mais aucun des deux n'a sa place dans un travail neuf. Utilisez SHA-256, que le Générateur de hachage calcule aussi bien pour des fichiers que pour du texte.
Les mots de passe posent un autre problème, et un hachage rapide est le mauvais outil précisément parce qu'il est rapide. Du matériel grand public réalise des milliards d'opérations SHA-256 par seconde : une table divulguée de hachages SHA-256 de mots de passe est donc une table divulguée de mots de passe. bcrypt, scrypt et Argon2id sont délibérément lents et réglables, dotés d'un facteur de travail (et, pour les deux derniers, d'un coût en mémoire) que vous relevez à mesure que le matériel progresse, et chacun stocke dans sa sortie un sel aléatoire unique.
Un hachage n'est pas non plus une signature. Pour prouver qu'un message vient de quelqu'un qui détient une clé, employez HMAC-SHA-256 plutôt que de hacher le secret et le message collés l'un à l'autre, ce qui est vulnérable à l'extension de longueur.
Ce qu'un hachage ne peut pas faire
Un hachage ne peut pas être inversé par le calcul. Il peut l'être par recherche dès que l'ensemble des entrées possibles est assez petit pour être énuméré : hachez tous les codes PIN à quatre chiffres et vous obtenez instantanément les dix mille empreintes, et il en va de même pour tous les codes postaux, numéros de téléphone ou adresses d'une liste divulguée. Hacher un identifiant ne l'anonymise pas.
Le salage, une valeur aléatoire unique stockée avec chaque enregistrement et mêlée à l'entrée, ne rend pas une cible donnée plus difficile à deviner, mais il oblige un attaquant à s'en prendre à chaque enregistrement séparément et prive les tables précalculées de toute valeur. Pour des valeurs à faible entropie, c'est le poivre (une clé secrète conservée en dehors de la base de données) qui aide véritablement.
Reconnaître une chaîne inconnue
| Forme | Indice | Exemple |
|---|---|---|
| Hexadécimal | Uniquement 0-9a-f, longueur paire. 32 caractères pour MD5, 40 pour SHA-1, 64 pour SHA-256 | 5d41402abc4b2a76b9719d911017c592 |
| Base64 | Casse mixte avec + et /, longueur multiple de 4, peut se terminer par = ou == | SGVsbG8sIHdvcmxkIQ== |
| Base64url | La même chose avec - et _, en général sans remplissage | eyJzdWIiOiIxMjM0NSJ9 |
| Encodé en pourcent | % suivi de deux chiffres hexadécimaux | a%20b%26c |
| JWT | Trois blocs Base64url séparés par des points, commençant par eyJ | eyJhbGciOi... |
| bcrypt | Exactement 60 caractères, commençant par $2a$, $2b$ ou $2y$ suivis d'un coût | $2b$12$... |
| Argon2 | $argon2id$v=19$m=...,t=...,p=...$salt$hash |
eyJ mérite d'être mémorisé : c'est le Base64 de {", si bien que tout bloc commençant ainsi est un objet JSON encodé, jeton ou non.
Un décodage réussi ne prouve pas que la supposition était bonne, car Base64 transforme n'importe quelle entrée en octets. Vérifiez que le résultat est un texte plausible, ou qu'il commence par une signature de fichier connue : un Base64 commençant par iVBORw0KGgo est un PNG, /9j/ un JPEG, JVBERi0 un PDF et UEsDB un zip. Si les octets ne ressemblent à rien, passez la sortie dans l'outil Texte en binaire pour lire les codes directement, ou vérifiez si la valeur avait été encodée en pourcent avant de l'être en Base64, un double emballage fréquent.