Un enlace secreto de un solo uso es una URL que muestra su contenido exactamente una vez y después deja de funcionar para siempre. El navegador de quien envía cifra el secreto con AES-GCM-256, el servidor guarda solo el texto cifrado y la clave viaja en la parte del enlace que va después de #k=, que los navegadores nunca envían. La primera vez que el destinatario lo muestra, se descifra y la copia almacenada se destruye en la misma operación.
Puedes crear uno sin cuenta con el generador de enlaces secretos gratis.
El mecanismo es sencillo, pero lo interesante está en los casos límite, que es donde las implementaciones descuidadas filtran datos sin que nadie se entere.
El flujo básico
- Quien envía pega un secreto. El navegador genera una clave aleatoria, cifra el contenido con AES-GCM-256 mediante la Web Crypto API y envía solo el texto cifrado. El servidor lo guarda con un slug aleatorio y difícil de adivinar y devuelve una URL; el navegador le añade la clave como fragmento
#k=, que nunca sale del dispositivo de quien envía. - Quien envía comparte la URL por cualquier canal: correo, chat, SMS.
- El destinatario abre la URL. Un
GETsimple devuelve solo el estado. Cuando hace clic en mostrar, el servidor marca el secreto como consumido y devuelve el texto cifrado en un solo paso indivisible; el navegador del destinatario lo descifra con la clave del fragmento. - Cualquier visita posterior recibe una página que indica que el secreto se ha destruido, en lugar del contenido.
El paso crítico es el 3. Si la lectura y el cambio de estado se hacen en dos consultas separadas (un SELECT para comprobar y luego un UPDATE para marcar), dos personas que lo abran a la vez pueden pasar la comprobación antes de que ninguna escriba. Las dos reciben el contenido. Es un error TOCTOU (time-of-check to time-of-use, del momento de la comprobación al momento del uso) clásico, y convierte la garantía de «exactamente una vez» en «casi siempre una vez».
¿Por qué la lectura y la destrucción deben ser un solo paso indivisible?
Porque, si no lo es, dos personas que lo abran a la vez pueden pasar la comprobación antes de que ninguna marque el secreto como consumido. La solución es reunir la comprobación, la marca y la lectura en una sola operación de base de datos. En Postgres, con UPDATE ... RETURNING, queda así:
UPDATE secrets
SET burned_at = now()
WHERE slug = $1
AND burned_at IS NULL
AND expires_at > now()
RETURNING payload;
El bloqueo de fila que toma el UPDATE obliga a que las llamadas simultáneas se ejecuten una tras otra: la primera cumple la cláusula WHERE, cambia burned_at y recibe el contenido mediante RETURNING. Cada llamada posterior encuentra burned_at ya establecido, no coincide con ninguna fila y recibe el estado de destruido. No hay ningún intervalo entre «comprobar» y «consumir», porque son la misma sentencia.
LinkPilot registra cada visualización en un solo paso indivisible, mediante una función de base de datos, precisamente por eso: varios intentos simultáneos de mostrar el secreto no pueden filtrarlo dos veces. Si estás evaluando una herramienta, es una pregunta razonable para hacerle directamente al proveedor: ¿la lectura y la destrucción ocurren en un solo paso indivisible?
¿Por qué los enlaces destruidos devuelven HTTP 410 y no 404?
Porque 410 Gone indica que el recurso existió y ya no está disponible de forma permanente, que es exactamente lo que es un secreto destruido. Una vez consumido, el enlace debe fallar de forma clara y definitiva. LinkPilot hace dos cosas al destruirlo:
- El contenido almacenado se sobrescribe con
[REDACTED]en la misma escritura que cambia el estado, así que ni siquiera un acceso indebido posterior a la base de datos puede recuperar un secreto ya gastado. - Los intentos posteriores de mostrarlo devuelven HTTP 410 Gone con una página clara que indica que ya se ha visto y destruido.
410 es el código de estado correcto: significa que el recurso existió y ya no está disponible, de forma intencionada y permanente, a diferencia de 404, que dice «no lo conozco». La diferencia también importa a las personas: un destinatario que ve «este secreto ya se ha visto» sabe que debe avisar a quien lo envió, que puede revisar el historial de auditoría y rotar la credencial si quien lo vio no era el destinatario.
Slugs difíciles de adivinar, y por qué no bastan
El slug es lo único que separa un texto cifrado sin leer del resto de internet (la clave va en el fragmento, no en el servidor), así que debe tener entropía real. Un slug generado con una fuente aleatoria criptográficamente segura y, por ejemplo, 22 caracteres en base62 tiene unos 128 bits: probar combinaciones no es un ataque realista a ningún ritmo de peticiones que un servidor pueda aguantar.
Pero la entropía del slug solo protege contra quien intenta adivinarlo. No hace nada si alguien ve la URL: el historial del navegador, los registros de un proxy, correos reenviados, pantallas compartidas, gestores del portapapeles. Por eso las implementaciones serias añaden varias capas:
- Expiración, para que una URL vista deje de funcionar en la fecha prevista.
- Una frase de contraseña, para que la URL vista no baste por sí sola.
- Destrucción tras la lectura, para que una URL vista no sirva de nada después de la lectura legítima.
Vistas previas de enlaces y bots
Las aplicaciones de chat (Slack, Teams, iMessage) y los analizadores de seguridad del correo generan la vista previa de las URL pidiéndolas con un GET en cuanto se entrega el mensaje. Si el endpoint que muestra el secreto lo destruye con un GET, el generador de vistas previas lo destruye antes de que la persona llegue a verlo. Es el error real más habitual de las herramientas de destrucción tras la lectura hechas sin cuidado.
Las defensas, tal como las implementa LinkPilot:
- Acciones separadas para los metadatos y para mostrar. Un
GETal endpoint de mostrar devuelve solo el estado y si hace falta una frase de contraseña. Solo se registra una visualización con unPOSTexplícito, que es lo que ocurre cuando una persona hace clic en el botón Mostrar. - Frase de contraseña (Pro). Si hay una frase de contraseña, no se devuelve el contenido a nadie, sea bot o persona, sin el hash correcto.
Cache-Control: no-storeen todas las respuestas que muestran un secreto, para que ningún intermediario guarde en caché su contenido.
La página de arquitectura de seguridad de LinkPilot documenta este tratamiento de principio a fin.
El ciclo de vida, estado por estado
| Estado | Lo provoca | Lo que ve un visitante | ¿Se puede recuperar el contenido? |
|---|---|---|---|
| Activo | La creación | Página para mostrarlo (con petición de frase de contraseña si la hay) | Sí, queda una lectura disponible |
| Destruido | Se alcanza el límite de visualizaciones | «Ya se ha visto y destruido», HTTP 410 | No, el contenido se ha sobrescrito |
| Expirado | Pasa la fecha de expiración | Página de estado expirado | No, se borra en el siguiente acceso |
| Revocado | Quien lo envió lo anula desde el panel | No se muestra, HTTP 410 | No, de forma inmediata y permanente |
Cada transición queda en el historial de auditoría, una función del plan Pro de LinkPilot. Los tipos de evento son created, viewed, failed_passphrase, burned, expired y revoked, cada uno con fecha y hora y una IP con hash (SHA-256 con una sal que cambia cada día; la IP original nunca se guarda). Ese registro es lo que convierte «creo que le llegó» en «visto a las 10:05 desde el país esperado, una visualización y después destruido».
Ampliaciones habituales
- Frase de contraseña: exige una frase de contraseña enviada por otro canal antes de mostrar el secreto, para que una URL filtrada no sea por sí sola un secreto filtrado. En LinkPilot es una función del plan Pro (29 USD al mes), junto con la expiración personalizada y el historial de auditoría; el generador sin cuenta no permite ponerla. Limita los intentos: LinkPilot permite 5 por IP, por minuto y por secreto, y registra cada intento fallido.
- Expiración por tiempo: el secreto deja de existir pasado un plazo aunque nadie lo haya leído. El formulario de LinkPilot con sesión iniciada ofrece 1 hora, 24 horas o 7 días en todos los planes, y fecha y hora personalizadas en Pro; el generador sin cuenta pone 7 días por defecto. Cubre el caso del enlace olvidado en la bandeja de entrada, que la destrucción tras la lectura no resuelve.
- Revocación manual: quien envía puede anular un secreto sin leer desde el panel en cuanto algo le parezca raro. Un seguro barato para cuando lo has enviado al Alex equivocado.
Casos límite que distinguen una buena implementación
Lo abre primero la persona equivocada
La destrucción tras la lectura no impide que lo vea quien no debe, pero hace visible el error de entrega. El destinatario legítimo llega a una página de secreto destruido, el historial de auditoría muestra una visualización que no esperabas y rotas la credencial. Es molesto, pero acotado: exactamente una exposición, detectada pronto. Compáralo con un secreto pegado en un chat, donde nunca lo sabrías.
El problema de las capturas de pantalla
Un solo uso significa una sola visualización, no una sola copia. El destinatario puede hacer una captura, copiar o volver a escribir el secreto; nada en el enlace lo impide, y los proveedores que dan a entender lo contrario venden ficción. Dicho con honestidad: la destrucción tras la lectura controla el canal de entrega, no al destinatario. La confianza en el destinatario es un punto de partida, no un resultado.
Precarga del navegador y carga especulativa
Algunos navegadores y aplicaciones precargan las URL en las que es probable que hagas clic. Como una herramienta bien hecha solo destruye el secreto con un POST explícito, la precarga se comporta igual que un generador de vistas previas: puede pedir la estructura de la página, nunca el contenido.
Cuándo no usar un enlace de un solo uso
- Vas a necesitar el dato otra vez. Poder recuperarlo es el objetivo de un gestor de contraseñas y justo lo contrario de lo que busca un enlace de un solo uso. Usa un elemento de la bóveda.
- Lo necesitan varias personas. Un enlace, una lectura. Si lo necesitan varias, o creas un enlace por persona (correcto y auditable) o reconoces que lo que necesitas es un control de acceso compartido.
- Lo consume una máquina. Los servicios y la CI deben leer de un gestor de secretos, no hacer clic en enlaces.
Si quieres ver el mecanismo en acción, crea uno con el generador de enlaces secretos gratis: sin cuenta, cifrado en tu navegador y destruido tras la primera lectura.
Lecturas relacionadas
- Compartir contraseñas de forma segura: 5 métodos comparados
- One Time Secret, enlace de un solo uso y enlace secreto: las páginas breves con la herramienta arriba
- ¿Es seguro OneTimeSecret?
- Las mejores alternativas a OneTimeSecret
- OneTimeSecret frente a Privnote frente a Password Pusher
- Enlaces que expiran: el secreto se destruye, no solo se redirige
- Generador de enlaces secretos gratis
Referencias
- W3C, Web Cryptography API.
- IETF, RFC 3986, sección 3.5 Fragment.
- IETF, RFC 9110: HTTP Semantics, sección 15.5.11 410 Gone.
- Documentación de PostgreSQL, UPDATE.
