Un lien secret à usage unique est une URL qui affiche son contenu une
seule fois, puis devient définitivement inerte. Le navigateur de
l’expéditeur chiffre le secret en AES-GCM-256, le serveur ne stocke que
le texte chiffré, la clé voyage dans la partie du lien située après
#k=, que les navigateurs n’envoient jamais, et la première ouverture
par le destinataire déchiffre le contenu et détruit la copie stockée
dans la même opération.
Vous pouvez en créer un sans compte avec le générateur de liens secrets gratuit.
Le mécanisme est simple. Ce sont les cas limites qui le rendent intéressant, et c’est là que les implémentations bâclées laissent discrètement fuir des données.
Le fonctionnement de base
- L’expéditeur colle un secret. Le navigateur génère une clé
aléatoire, chiffre le contenu en AES-GCM-256 avec la
Web Crypto API et n’envoie
que le texte chiffré. Le serveur le stocke sous un slug aléatoire et
difficile à deviner, puis renvoie une URL ; le navigateur y ajoute la
clé sous forme de fragment
#k=, qui ne quitte jamais l’appareil de l’expéditeur. - L’expéditeur partage l’URL par n’importe quel canal : e-mail, messagerie, SMS.
- Le destinataire ouvre l’URL. Une simple requête
GETne renvoie que le statut. Quand il clique sur le bouton Afficher, le serveur marque le secret comme consommé et renvoie le texte chiffré en une seule opération indivisible ; le navigateur du destinataire le déchiffre avec la clé tirée du fragment. - Toute visite suivante n’obtient plus le contenu, seulement une page indiquant qu’il a été détruit.
L’étape critique est l’étape 3. Si la lecture et la mise à jour du
statut passent par deux requêtes distinctes (SELECT pour vérifier,
puis UPDATE pour marquer), deux personnes qui ouvrent le lien en même
temps peuvent toutes deux passer la vérification avant que l’une
d’elles n’écrive. Chacune obtient alors le contenu. C’est un bogue
TOCTOU classique (time-of-check to time-of-use, l’écart entre le moment
de la vérification et celui de l’utilisation), et il transforme votre
garantie « exactement une fois » en « une fois, en général ».
Pourquoi la destruction et la lecture doivent-elles former une seule opération ?
Parce que sinon, deux personnes qui ouvrent le lien en même temps
peuvent toutes deux passer la vérification avant que l’une d’elles ne
marque le secret comme consommé. La solution consiste à réunir la
vérification, le marquage et la lecture en une seule opération de base
de données. Dans Postgres, avec
UPDATE ... RETURNING,
cela donne :
UPDATE secrets
SET burned_at = now()
WHERE slug = $1
AND burned_at IS NULL
AND expires_at > now()
RETURNING payload;
Le verrou de ligne que pose l’UPDATE oblige les appels simultanés à
passer l’un après l’autre : le premier satisfait la clause WHERE,
modifie burned_at et récupère le contenu grâce à RETURNING. Chaque
appel suivant trouve burned_at déjà renseigné, ne correspond à aucune
ligne et reçoit le statut « détruit ». Il n’existe aucun intervalle
entre « vérifier » et « consommer », puisqu’il s’agit de la même
instruction.
C’est précisément pour cette raison que LinkPilot enregistre chaque ouverture en une seule opération indivisible, au moyen d’une fonction de base de données : des tentatives d’affichage simultanées ne peuvent pas laisser fuir le secret deux fois. Si vous évaluez un outil, posez directement la question à l’éditeur : la lecture et la destruction se font-elles en une seule étape indivisible ?
Pourquoi un lien détruit renvoie-t-il HTTP 410 et non 404 ?
Parce que 410 Gone indique que la ressource a existé et qu’elle est définitivement indisponible, ce qui correspond exactement à un secret détruit. Une fois consommé, le lien doit échouer de façon visible et définitive. Au moment de la destruction, LinkPilot fait deux choses :
- Le contenu stocké est écrasé par
[REDACTED]dans la même écriture que celle qui change le statut : même une compromission ultérieure de la base de données ne permet pas de récupérer un secret déjà consommé. - Les tentatives d’affichage suivantes renvoient HTTP 410 Gone, avec une page claire indiquant que le contenu « a déjà été consulté et détruit ».
410 est le bon code de statut : il signifie que la ressource a existé et qu’elle est indisponible volontairement et définitivement, alors que 404 dit « jamais entendu parler ». La distinction compte aussi pour les personnes : un destinataire qui lit « ce secret a déjà été consulté » sait qu’il doit prévenir l’expéditeur, qui peut alors vérifier l’historique d’audit et renouveler le secret si la personne qui l’a ouvert n’était pas le destinataire.
Des slugs difficiles à deviner, et pourquoi cela ne suffit pas
Le slug, c’est-à-dire la fin du lien, est tout ce qui sépare l’internet public d’un texte chiffré non lu (la clé se trouve dans le fragment, pas sur le serveur) : il doit donc contenir une véritable entropie. Un slug tiré d’une source aléatoire cryptographiquement sûre, avec par exemple 22 caractères en base62, porte environ 128 bits : l’énumération n’est pas une attaque réaliste, quel que soit le débit de requêtes qu’un serveur pourrait encaisser.
Mais l’entropie du slug ne protège que contre le fait de deviner l’URL. Elle ne fait rien contre le fait qu’on voie l’URL : historique du navigateur, journaux de proxy, e-mails transférés, partages d’écran, gestionnaires de presse-papiers. C’est pourquoi les implémentations sérieuses ajoutent d’autres protections :
- L’expiration, pour qu’une URL repérée cesse de fonctionner à l’heure prévue.
- Une phrase de passe, pour qu’une URL repérée ne suffise pas à elle seule.
- La destruction après lecture, pour qu’une URL repérée ne vaille plus rien après la lecture légitime.
Aperçus de liens et robots
Les messageries (Slack, Teams, iMessage) et les scanners de sécurité
des e-mails génèrent un aperçu des liens en récupérant les URL par une
requête GET dès qu’un message est remis. Si votre endpoint d’affichage
détruit le secret sur un GET, l’outil d’aperçu de liens le détruit avant même
que la personne ne le voie. C’est la défaillance la plus courante, dans
la pratique, des outils naïfs de destruction après lecture.
Les protections, telles que LinkPilot les met en œuvre :
- Séparer les métadonnées et l’affichage. Un
GETsur l’endpoint d’affichage ne renvoie que le statut et indique si une phrase de passe est requise. Une ouverture n’est enregistrée que sur unPOSTexplicite, l’action d’une personne qui clique sur le bouton Afficher. - Phrase de passe obligatoire (Pro). Quand une phrase de passe est définie, aucun contenu n’est renvoyé à qui que ce soit, robot ou humain, sans le bon hash.
Cache-Control: no-storesur chaque réponse d’affichage, pour que les intermédiaires ne mettent jamais en cache le contenu d’un secret.
La page Architecture de sécurité de LinkPilot décrit ce traitement de bout en bout.
Le cycle de vie, état par état
| État | Déclencheur | Ce que voit le visiteur | Contenu récupérable ? |
|---|---|---|---|
| « active » (actif) | Création | Page d’affichage (demande de phrase de passe si elle est définie) | Oui : un affichage disponible |
| Détruit | Limite d’ouvertures atteinte | « Déjà consulté et détruit » : HTTP 410 | Non : contenu écrasé |
| « expired » (expiré) | Date d’expiration dépassée | Page indiquant que le lien a expiré | Non : expurgé au prochain accès |
| « revoked » (révoqué) | L’expéditeur le désactive depuis le tableau de bord | Refus d’affichage : HTTP 410 | Non : immédiat et définitif |
Chaque transition est inscrite dans l’historique d’audit, une
fonctionnalité Pro de LinkPilot. Les types d’événements sont created,
viewed, failed_passphrase, burned, expired et revoked, chacun
avec un horodatage et une adresse IP hachée (SHA-256 avec un sel
renouvelé chaque jour ; l’adresse IP brute n’est jamais stockée). C’est
cette trace qui transforme « je pense qu’il l’a reçu » en « ouvert à
10 h 05 depuis le pays attendu, une seule ouverture, puis détruit ».
Extensions courantes
- Phrase de passe obligatoire : exiger une phrase de passe transmise par un autre canal avant l’affichage, pour qu’une URL divulguée ne suffise pas à divulguer le secret. Dans LinkPilot, c’est une fonctionnalité Pro (29 $/mois), avec l’expiration personnalisée et l’historique d’audit ; le générateur sans compte ne permet pas d’en définir une. Limitez le nombre de tentatives : LinkPilot en autorise 5 par adresse IP, par minute et par secret, et enregistre chaque échec.
- Expiration dans le temps : le secret disparaît au bout d’un délai fixé, même s’il n’a jamais été lu. Le formulaire de LinkPilot, une fois connecté, propose 1 heure, 24 heures ou 7 jours sur tous les forfaits, avec une date et une heure personnalisées sur Pro ; le générateur sans compte fixe 7 jours par défaut. Cela couvre le cas du lien oublié dans une boîte de réception, que la destruction après lecture ne règle pas.
- Révocation manuelle : l’expéditeur peut désactiver un secret non lu depuis le tableau de bord dès que quelque chose lui semble anormal. Une assurance peu coûteuse pour le cas « je l’ai envoyé au mauvais Alex ».
Les cas limites qui distinguent les bonnes implémentations
La mauvaise personne l’ouvre en premier
La destruction après lecture n’empêche pas la mauvaise personne d’ouvrir le lien : elle rend l’erreur d’envoi visible. Le destinataire prévu tombe sur une page indiquant que le contenu a été détruit, l’historique d’audit montre une ouverture inattendue, et vous renouvelez les identifiants. C’est pénible, mais limité : une seule exposition, détectée rapidement. Avec un secret collé dans une messagerie, vous ne l’auriez jamais su.
Le problème de la capture d’écran
Usage unique signifie un seul affichage, pas une seule copie. Le destinataire peut faire une capture d’écran du secret, le copier ou le recopier à la main : rien dans le lien ne l’empêche, et les éditeurs qui laissent entendre le contraire vendent une fiction. Pour être honnête : la destruction après lecture contrôle le canal de transmission, pas le destinataire. La confiance envers le destinataire est une condition de départ, pas un résultat.
Préchargement et chargement anticipé du navigateur
Certains navigateurs et certaines applications préchargent les URL sur
lesquelles l’utilisateur a des chances de cliquer. Comme un outil bien
conçu ne détruit le secret que sur un POST explicite, le préchargement
se comporte exactement comme un outil d’aperçu de liens : il peut récupérer
l’ossature de la page, jamais le contenu.
Quand ne pas utiliser un lien à usage unique
- La valeur servira de nouveau. Pouvoir la récupérer est tout l’objet d’un gestionnaire de mots de passe, et exactement ce qu’un lien à usage unique cherche à empêcher. Utilisez un élément de coffre-fort.
- Plusieurs destinataires en ont besoin. Un lien, une lecture. Diffuser à plusieurs personnes implique soit de créer un lien par personne (solution correcte et traçable), soit d’admettre qu’il vous faut plutôt un contrôle d’accès partagé.
- Le destinataire est une machine. Les services et la CI doivent lire les secrets dans un gestionnaire de secrets, pas cliquer sur des liens.
Pour voir le mécanisme fonctionner, créez un lien avec le générateur de liens secrets gratuit : sans compte, chiffré dans votre navigateur, détruit après la première ouverture.
Pour aller plus loin
- Partager un mot de passe en toute sécurité : 5 méthodes comparées
- One Time Secret, lien à usage unique et lien secret : les pages courtes avec l’outil en haut
- OneTimeSecret est-il sûr ?
- Les meilleures alternatives à OneTimeSecret
- OneTimeSecret vs Privnote vs Password Pusher
- Liens qui expirent : le secret est détruit, pas seulement redirigé
- Générateur de liens secrets gratuit
Références
- W3C, Web Cryptography API.
- IETF, RFC 3986, section 3.5 Fragment.
- IETF, RFC 9110: HTTP Semantics, section 15.5.11 410 Gone.
- PostgreSQL Documentation, UPDATE.
