01 / Mes articles / Publié
Une protection que je n'ai pas vue refuser n'existe pas
J'ai construit un site privé, puis j'ai essayé de prouver qu'il fuyait : deux de mes protections ne tenaient pas.
Rédigé le
J'ai construit un site destiné à trois personnes, pour une durée d'une semaine. Tout le contenu devait rester derrière un mot de passe, y compris les images et la vidéo. Rien ne devait subsister après la date limite.
Les besoins étaient donc inversés par rapport à un site normal. Je ne cherchais pas de l'audience, je cherchais l'absence de fuite. Et une absence, ça ne se constate pas en regardant le site fonctionner.
Le principe que j'ai fini par adopter
Une vérification qui réussit toujours ne vérifie rien.
Quand j'écris un contrôle, je commence par lui donner une raison d'échouer. J'introduis volontairement le défaut qu'il est censé détecter, je vérifie qu'il le détecte, puis je retire le défaut. Si le contrôle passe au vert dans les deux cas, il ne sert à rien, et il est pire qu'inutile : il me rassure.
J'ai appliqué ce principe à deux endroits. Les deux fois, j'ai trouvé un défaut réel.
Premier contrôle : rien du contenu ne doit atteindre le navigateur
Mes textes vivent dans des fichiers typés, importés uniquement par des composants rendus côté serveur. En théorie, aucune phrase ne peut se retrouver dans le JavaScript envoyé au navigateur. En théorie.
J'ai écrit une vérification en deux parties. La première parcourt le graphe d'imports et échoue si un composant marqué client remonte jusqu'à un fichier de contenu. La seconde cherche le texte lui-même dans les fichiers compilés.
Pour la tester, j'ai injecté une fuite dans une page, puis lancé la vérification. Elle a échoué, comme prévu. J'ai ensuite injecté une fuite partielle : non pas un texte entier, mais une phrase prise au milieu d'un paragraphe.
La vérification est passée au vert.
Elle cherchait des chaînes complètes. Une phrase isolée, une citation, une troncature, tout cela lui échappait. Je l'ai réécrite pour chercher aussi des phrases entières et des fenêtres glissantes de six mots consécutifs, plus les variantes encodées que produit la compilation. J'ai rejoué les deux injections : les deux sont détectées.
Sans ce second essai, j'aurais eu une vérification verte et une fuite possible. C'est exactement le scénario que je redoute, parce qu'il ne ressemble pas à une panne.
Deuxième contrôle : refuser ce qui vient d'ailleurs
Ma page de connexion refuse les requêtes venues d'un autre site. J'ai écrit le contrôle, je l'ai testé en ligne de commande avec une origine étrangère : refusé. Avec la bonne origine : accepté. Contrôle vert.
Puis j'ai ouvert un navigateur. Impossible de me connecter.
Mon site envoie un en-tête qui demande au navigateur de ne transmettre aucun référent. C'est une bonne pratique, je l'avais mise pour éviter de révéler l'adresse de mes pages. Mais avec cette consigne, le navigateur annonce une origine vide au lieu de la vraie. Mon contrôle voyait donc une origine inconnue et refusait tout le monde, alors que mes essais en ligne de commande, eux, envoyaient sagement la bonne origine.
Ma vérification ne testait pas ce que fait un navigateur. Elle testait ce que fait mon outil de test.
Je me suis appuyé sur un autre en-tête, que les navigateurs envoient et qui dit si la requête vient du même site, en gardant l'origine comme solution de repli. Puis j'ai refait les deux essais dans un vrai navigateur : le formulaire hébergé ailleurs est refusé, le mien passe.
Ce que ces erreurs ont en commun
Dans les deux cas, ma protection existait, mon test existait, et ils ne se rencontraient pas.
Le premier test vérifiait une forme de fuite au lieu de vérifier la fuite. Le second vérifiait le comportement d'un client qui n'est pas celui de mes utilisateurs. Ni l'un ni l'autre n'aurait été trouvé en relisant le code, parce que le code était correct par rapport à ce que je croyais tester.
J'en ai tiré trois habitudes que je garde.
J'écris le contrôle négatif avant le contrôle positif. Je fabrique d'abord le défaut, je regarde le test échouer, et seulement ensuite je corrige. Un test qui n'a jamais échoué sous mes yeux n'a pas de valeur démontrée.
Je teste avec le client réel. Une ligne de commande n'envoie pas les mêmes en-têtes qu'un navigateur, ne gère pas les mêmes règles de cookies, et ne subit pas les consignes que j'envoie moi-même. Pour tout ce qui touche à l'authentification, je finis dans un navigateur.
Je préfère un contrôle bruyant à un contrôle élégant. Ma vérification de fuite cherche des fenêtres de six mots, ce qui produit quelques fausses alertes quand une tournure banale se retrouve dans une bibliothèque. Je préfère écarter trois alertes à la main plutôt que manquer la seule qui compte.
La limite que je connais
Ces vérifications prouvent l'absence de ce que je sais chercher. Elles ne prouvent pas l'absence de fuite. Une donnée qui sortirait par un chemin auquel je n'ai pas pensé passerait au vert sans difficulté.
C'est pour ça que je ne présente jamais un résultat vert comme une garantie. Je le présente pour ce qu'il est : la liste de ce que j'ai essayé de casser, et qui a tenu.
Périmètre de la mesure
2 contrôles. Contrôles essayés sur mon site privé, chacun ayant révélé un défaut réel.
6 mots consécutifs. Taille des fenêtres glissantes utilisées dans ma recherche de fuites de texte.