Pourquoi le fragment après # d’une URL convient pour une clé, et quand cette protection lâche

Dans la barre d’adresse, point d’interrogation et dièse ne sont qu’un caractère d’écart. La query après ? entre dans la ligne de requête HTTP : serveur, reverse proxy et journaux d’accès la voient. Le fragment après # reste, selon la spécification, dans l’onglet courant ; il ne part pas avec cette requête de document. Mettre la clé de déchiffrement derrière le dièse empêche l’hébergeur de la voir. Cela n’empêche personne de transférer le lien entier.

Retenez d’abord un contrôle que vous pouvez finir dans un onglet. Ouvrez Network dans les outils de développement et comparez toute la barre d’adresse à la ligne de requête du document. La barre d’adresse peut afficher la clé après #. Cette même chaîne ne doit pas apparaître dans la ligne de requête. Si elle y est, ce n’est pas le comportement par défaut du navigateur : la page a écrit le fragment dans une query, ou un script l’a lu puis réinjecté dans une requête.

Point d’interrogation et dièse ne protègent pas la même chose

Les liens chiffrés à usage unique se résument trop souvent à : « la clé est dans l’URL ». La phrase est trop large. Elle superpose deux faits distincts. Une URL peut porter à la fois une query et un fragment : ? ouvre la query, # ouvre le fragment. Les deux s’affichent dans la barre d’adresse, et qui copie emporte en général la chaîne entière. HTTP, lui, ne les traite pas de la même façon.

La query devient une partie de la cible de la requête. Vous ouvrez s.html?id=abc123 : serveur, reverse proxy, CDN et journal d’accès voient id=abc123 par conception. Le fragment n’entre pas dans cette requête de document. Vous ouvrez s.html?id=abc123#la-clé : le navigateur demande la page qui porte cet id. Tout ce qui suit le dièse reste dans l’onglet, pour que le script de la page le lise avec location.hash.

« La clé est dans l’URL » répond donc seulement à : un humain peut-il voir la chaîne entière ? Elle ne dit pas si le serveur peut voir la clé. L’endroit qui convient pour une clé, c’est le fragment après le dièse — à condition que l’implémentation l’y laisse vraiment, au lieu de le glisser dans la query parce que c’était plus simple.

Ce que disent les spécifications : le fragment reste côté client

Ce n’est pas une convention privée de produit. La RFC 3986, section 3.5 définit le fragment comme identifiant d’une « ressource secondaire » : le dièse apparaît, puis le reste de l’URI. La façon d’interpréter ce fragment dépend du type de document récupéré. C’est le client qui le traite. Le schéma d’URI ne le réécrit pas.

La RFC 9110, section 7.1 applique la même règle à HTTP : l’URI cible que le navigateur en déduit ne contient pas de fragment, parce que l’identifiant de fragment est destiné au client. Quand le navigateur demande le document, la ligne de requête ne doit contenir ni le dièse, ni ce qui le suit.

Le Referer suit la même coupure. La RFC 9110 exige qu’un agent utilisateur qui génère Referer n’y mette ni fragment ni userinfo. La page MDN sur Referer le confirme : l’en-tête peut porter l’origine, le chemin et la query, pas un fragment #. La Referrer Policy du W3C, lorsqu’elle transforme une URL en referrer, vide d’abord le fragment, puis décide si le chemin et la query restent.

Ces trois textes prouvent une seule chose : un navigateur conforme n’écrit pas la clé derrière le dièse dans la ligne de requête HTTP, ni dans Referer. Ils ne prouvent pas que les scripts de la page, les extensions, ou le texte que vous collez ailleurs, enlèvent d’eux-mêmes ce morceau.

Si la clé est dans la query, elle est en clair dans les journaux

Écrire la clé en ?id=abc123&key=... est plus simple à livrer. Le serveur peut, avec la même query, récupérer le chiffré et le déchiffrer. C’est courant sur les pages « chiffrer en ligne » qui veulent tout faire en une requête. C’est l’inverse de « l’hébergeur ne doit jamais voir la clé ».

Une query apparaît dans la ligne de requête. Elle entre aussi dans la plupart des journaux d’accès, des logs de reverse proxy, et une partie des rapports CDN. Une fois la clé dans la query, « nous ne stockons que du chiffré » est déjà faux au niveau des logs : un exploitant qui ouvre le journal du jour a la seconde moitié nécessaire au déchiffrement.

On proposera de passer la clé dans un corps POST. Cela peut l’écarter des journaux d’URL. La clé a tout de même quitté le navigateur et atteint la machine que vous appelez « zéro connaissance ». L’intérêt du fragment est l’inverse : la clé n’a pas à partir comme champ HTTP. La page lit location.hash dans cet onglet.

Où vous la mettez Un humain la voit-il ? Cette requête HTTP la voit-elle ?
Query après ? Oui, barre d’adresse et texte copié Oui. Ligne de requête, proxys et journaux d’accès l’enregistrent
Fragment après # Oui, barre d’adresse et texte copié Par défaut, non. Ligne de requête et Referer l’omettent selon la spec
Champ POST Pas dans la barre d’adresse Oui. Le corps atteint le serveur
Mémoire locale seulement, jamais écrite dans une URL L’autre personne ne la voit pas, sauf autre canal Non. Un lien à ouvrir d’un clic ne peut pas non plus finir la passation

La découpe qui tient : localiser avec la query, déchiffrer avec le dièse

Un lien chiffré à usage unique doit faire deux choses à la fois : dire au serveur quel chiffré récupérer, et dire au navigateur du destinataire quelle clé utiliser en local. Ces deux tâches ne doivent pas partager le même champ HTTP.

La découpe vérifiable est s.html?id={id}#{key}. La query ne porte que l’id de localisation. Le dièse ne porte que la clé. À la création, le navigateur exécute AES-256-GCM via Web Crypto ; seul le chiffré est autorisé à partir. À la lecture, le script lit location.hash, demande le chiffré au serveur, et déchiffre dans cet onglet. Le serveur, par conception, ne stocke le chiffré que le temps de la lecture, puis le détruit.

Le lien auto-destructible MakePwd suit cette frontière. La page de création et la page de lecture s’ouvrent sans compte. La page de lecture est publique pour le destinataire ; elle n’exige pas de connexion. Ce n’est encore qu’un choix d’implémentation, pas « un dièse égale un protocole de chiffrement ». Un fragment n’offre pas de confidentialité à lui seul. Il évite seulement que la clé quitte l’onglet comme champ HTTP.

N’utilisez pas de vraie phrase secrète, de numéro de pièce d’identité ou de tableau non masqué pour l’essai. Préparez une phrase jetable et un lien à usage unique généré pour ce contrôle. On compare une ligne de requête et une barre d’adresse ; on ne rediffuse pas un secret.

Contrôler sur place avec Network et la barre d’adresse

La note précédente expliquait comment, avec une chaîne canari, vérifier que le texte en clair n’avait pas quitté l’appareil comme donnée métier. Celle-ci resserre la question : la clé est-elle entrée dans HTTP ? Les étapes se font seules. Vous n’avez pas à lire l’autre article d’abord.

Créez un texte de test inoffensif et copiez le lien complet généré. Repérez le dièse : devant, s.html?id=... ; derrière, la clé. Ouvrez un onglet propre, ouvrez les outils de développement, cochez Preserve log, puis collez le lien.

  1. Comparez la barre d’adresse et la ligne de requête du document. La barre d’adresse doit garder # et ce qui suit. L’URL du document ne doit montrer que le chemin et ?id= — pas la clé après le dièse.
  2. Inspectez ensuite les XHR / Fetch. L’API qui récupère le chiffré peut porter id par conception. La même clé ne doit pas réapparaître dans un corps ou une query. Le body de création doit être du chiffré, pas la phrase de test que vous venez de saisir.
  3. Ouvrez à part query et body des envois d’analyse. Un chemin de page peut apparaître. Si un script écrit tout le location.href, la clé passe de « le comportement HTTP par défaut l’a cachée » à « la page l’a signalée ». C’est un défaut d’implémentation, pas un échec de la spécification.

Trois passages propres ne soutiennent qu’une conclusion étroite : dans ce navigateur, à cette ouverture, la clé n’a pas quitté l’onglet comme champ HTTP observé. Changez de navigateur, de version ou de script de page, et il faut recommencer.

Quand cette couche de protection lâche

Le fragment bloque « cette requête HTTP a remis la clé à l’hébergeur ». Dans les cas ci-dessous, la clé ne s’appuyait déjà plus sur le comportement HTTP par défaut. La spécification n’y peut rien.

Premier cas : le lien complet est collé dans un chat, un mail ou un ticket. L’autre côté voit la chaîne entière à l’œil nu, et ce qui suit le dièse entre dans l’historique de ce système. Certains clients jettent le hash et n’affichent que ce qui précède — le destinataire ouvre alors une page sans clé, et la page de lecture doit le dire, au lieu de redemander une clé au serveur. Dans les deux variantes, vous avez déjà remis un jeton au porteur à un autre système.

Deuxième cas : les scripts de la page et les extensions peuvent lire location.hash. C’est ce qui permet à la page de lecture de fonctionner. C’est aussi ce qui permet à une XSS ou à une extension hostile de prendre la clé. La RFC 9110, section 17.11, le rappelle : les fragments n’entrent pas dans la requête, mais restent visibles pour l’agent utilisateur, les extensions, et les scripts arrivés avec la réponse. Si une redirection hérite du fragment de l’URL d’origine, elle peut aussi emporter le fragment de ce site vers une autre origine.

Troisième cas : historique du navigateur, partage d’écran et presse-papiers. La chaîne entière de la barre d’adresse atterrit dans l’historique local. Projetez l’onglet dans une salle de réunion, et ce qui suit le dièse est sur le mur. Rien de tout cela ne passe par votre serveur, et « le fragment n’entre pas dans HTTP » ne le réfute pas.

Quatrième cas : pages de redirection ou raccourcisseurs qui réécrivent l’URL. Si la page intermédiaire ne transmet que le chemin et la query, le hash a déjà disparu à l’ouverture. Si elle lit d’abord le href complet en JavaScript puis redirige, la clé est entrée dans le front de cet intermédiaire. Les liens courts surtout se vérifient sur place : la chaîne que vous donnez est-elle encore l’originale, avec son # ?

Trois phrases qui inversent le raisonnement

« Le dièse est plus sûr, donc on peut transférer la chaîne entière sans y penser. » Non. Plus sûr vis-à-vis du serveur n’est pas plus sûr vis-à-vis d’un fil de discussion. L’URL complète est un justificatif. Quiconque la possède peut ouvrir la page de lecture et déchiffrer.

« Le Referer risque de fuiter la clé vers un lien sortant. » Selon les spécifications actuelles, un navigateur qui génère Referer doit retirer le fragment. Ce qu’il faut vraiment surveiller, c’est la page qui écrit location.href dans une analyse, un journal ou un script tiers. Le contrôle reste le contenu de Network, pas l’hypothèse « tout lien sortant fuit la clé ».

« La page de lecture devrait exiger une connexion, sinon n’importe qui peut l’ouvrir. » Cela mélange « qui tient le lien complet » et « qui a un compte ». Le contrôle d’accès d’un lien chiffré à usage unique, c’est la chaîne elle-même. Imposer une connexion au destinataire n’empêche pas le serveur de voir la clé — la clé ne doit jamais lui être envoyée. Chez MakePwd, la page de lecture est publique pour le destinataire. Création et lecture se font sans compte.

Questions fréquentes

Le texte après le dièse part-il vers le serveur ?

Par défaut, non. La RFC 9110 exclut le fragment de l’URI cible. Dans Network, la ligne de requête du document ne doit montrer ni # ni la clé qui le suit. Le serveur, par conception, ne voit que le chemin et ?id=.

Pourquoi ne pas mettre la clé après le point d’interrogation ?

La query entre dans la ligne de requête HTTP. L’hébergeur et les journaux d’accès la voient. Dès que la clé est dans la query, « chiffré seulement » est déjà faux au niveau des logs. Un id de localisation peut rester après ?. La clé, elle, doit rester après #.

Coller le lien complet dans un chat reste-t-il sûr ?

Le serveur ne voit toujours pas la clé. L’historique du chat, le ticket et l’historique du navigateur, si. L’URL complète est un jeton au porteur. S’il faut la transmettre, confirmez que l’autre personne a bien reçu la chaîne originale avec le dièse, et tenez-la pour quelque chose qui vivra dans son historique.

Faut-il un compte pour ouvrir la page de lecture ?

Non. La page de lecture est publique pour le destinataire : l’id de la query récupère le chiffré, la clé derrière le dièse déchiffre dans cet onglet. MakePwd n’a ni compte ni coffre de mots de passe. Création et lecture s’ouvrent sans inscription.

Trois points à retenir avant le prochain secret à usage unique

Premier : regarder le caractère, pas le slogan « la clé est dans l’URL ». La query entre dans HTTP. Le dièse, par défaut, non. Deuxième : à la création, confirmer que ce qui part est du chiffré ; à la lecture, confirmer que la ligne de requête n’inclut pas ce qui suit #. Troisième : avant de remettre le lien complet, demander s’il va atterrir dans un chat, un ticket, ou un raccourcisseur qui jette le hash. Ces deux derniers échecs n’ont rien à voir avec le « zéro connaissance » du serveur.

S’il faut aussi vérifier que le texte en clair n’a pas quitté l’onglet comme donnée métier, c’est le sujet de la note précédente : chercher le canari dans les corps Network et les envois d’analyse. Ici, on ne fait que transformer « pourquoi une clé peut rester derrière le dièse » en un jugement que vous pouvez confronter aux spécifications et à la ligne de requête.