Un enlace de un solo uso que se destruye con un simple GET muere en
cuanto Slack, Teams o una pasarela de correo lo abre para generar una
vista previa. La solución es devolver solo metadatos con GET y
destruir el secreto con un POST explícito para mostrarlo, con una
frase de contraseña enviada por un segundo canal como red de
seguridad. Este artículo explica qué sistemas abren enlaces sin
preguntar y cómo diseñar pensando en todos ellos.
El generador de enlaces secretos de un solo uso de LinkPilot funciona así: una petición para generar una vista previa solo recibe la estructura de la página; el secreto se descarga, y la vista se cuenta, cuando un navegador ejecuta la página.
El error por defecto
La mayoría de los clientes de chat y de correo intentan generar una vista previa cuando ven una URL. El proceso es este:
- Alguien pega el enlace secreto en Slack.
- El generador de vistas previas de Slack abre la URL (hace un
GET; Slack documenta este comportamiento en link unfurling). - El código que muestra el secreto trata la petición como la visita de una persona, destruye el secreto y devuelve el contenido.
- Slack recibe el contenido ya destruido (o una página genérica) y lo muestra.
- La persona hace clic en el enlace, ve «ya visto» y le pide otro al remitente.
El generador de vistas previas no tenía mala intención. El código confundió «alguien ha abierto la URL» con «el destinatario ha leído el secreto». Son hechos distintos, y todo el problema de diseño consiste en mantenerlos separados.
En el paso 3 se esconde una variante peor: si el GET del generador
de vistas previas recibió el contenido real, el secreto está ahora
dentro de la infraestructura de un tercero que genera vistas previas.
En el mejor de los casos, se descartó después de crear la vista
previa. Pero no tienes forma de confirmarlo, ni ningún evento de
auditoría que te avise de que ocurrió.
¿Quién abre un enlace antes que el destinatario?
Los generadores de vistas previas de los chats, las pasarelas de seguridad del correo, la protección de los dispositivos, los proxies corporativos y las extensiones de vista previa, y casi siempre más de uno. Decir que «Slack genera vistas previas» se queda corto. Una URL pegada en un entorno de trabajo actual suele pasar por varios sistemas antes de que una persona haga clic:
| Quién la abre | Cuándo | ¿Se puede identificar? |
|---|---|---|
| Generadores de vistas previas de chat (Slack, Teams, iMessage, WhatsApp, LinkedIn) | En cuanto se envía el mensaje | Normalmente sí, con user agents propios como Slackbot-LinkExpanding |
| Pasarelas de seguridad del correo (Mimecast, Proofpoint, Microsoft Defender) | Al entregar el correo, y a menudo otra vez en el momento del clic | A menudo no: muchas usan user agents de navegadores reales |
| Protección del dispositivo / antivirus | Cuando la URL llega al dispositivo del destinatario | Rara vez |
| Proxies web corporativos | En el momento del clic, antes de reenviar la petición de la persona | Rara vez |
| Extensiones de navegador que muestran vistas previas de enlaces | Al pasar el ratón o al cargar la página | No: usan la propia sesión del navegador de la persona |
El comportamiento de cada proveedor en esta tabla cambia con el
tiempo, así que toma lo que se dice de cada pasarela como «según lo
publicado en julio de 2026; compruébalo con la documentación actual».
Lo que tienen en común no cambia: todos estos sistemas hacen un simple
GET, porque es la única forma segura y universal de ver una URL que
no controlas.
¿Cómo evitas que una vista previa destruya un enlace de un solo uso?
Responde con metadatos al GET y destruye el secreto solo con un
POST explícito. La semántica de HTTP ya lo prevé:
RFC 9110, apartado 9.2.1
define GET como seguro e idempotente, es decir, una petición que
cualquiera puede hacer libremente porque no debe cambiar nada. POST
es el verbo de acción. Así que trata el acto de mostrar el secreto
como una acción POST detrás de un botón visible:
GET /s/abc123 → HTML page: "Click to reveal this secret"
POST /s/abc123 → burns the secret, returns the payload
Ahora el GET de Slack devuelve una página neutra con metadatos. El
secreto solo se destruye cuando la persona hace clic en el botón.
Cualquier escáner que respete la semántica de HTTP no llega a tocar el
contenido. Eso incluye a casi todos, porque hacer POST con efectos
sobre URL arbitrarias rompería media web.
Un recorrido paso a paso
Sigamos un enlace de principio a fin. Creas un secreto que se destruye tras la primera lectura y tiene frase de contraseña, y pegas la URL en un mensaje directo de Slack y en un correo.
- El generador de vistas previas de Slack hace
GET /s/abc123. Respuesta: una pequeña página de metadatos (estado activo, frase de contraseña obligatoria). No se registra ninguna vista. El secreto sigue vivo. - La pasarela de correo del destinatario reescribe la URL del
correo y la abre en el momento de la entrega para analizar su
reputación. El mismo
GET, los mismos metadatos. Sigue vivo. - El destinatario hace clic en el enlace del correo. El escáner
de la pasarela la abre una vez más en el momento del clic (
GET) y después redirige al navegador. - El navegador carga la página para mostrar el secreto, con otro
GET. La página muestra un botón para mostrarlo y un campo para la frase de contraseña. Sigue sin registrarse ninguna vista. - El destinatario escribe la frase de contraseña y hace clic en el
botón. El navegador hace
POST /s/abc123. Se registra la vista, se devuelve el contenido y el secreto se destruye. - Quien abra la URL después recibe una respuesta explícita de «ya visto y destruido» en lugar del contenido.
Cinco peticiones, una sola lectura. Esa proporción es normal en un entorno corporativo. Un diseño que no la tolere fallará el primer día, y el remitente acabará pegando la contraseña directamente en el chat.
Defensas por capas
La separación entre GET y POST
Cubre todos los generadores de vistas previas y escáneres que se portan bien, que son la inmensa mayoría del tráfico automatizado. Es la base; todo lo que sigue añade profundidad.
La frase de contraseña
Cubre lo que la separación no puede: sandboxes que ejecutan JavaScript e interactúan con la página, además de clics accidentales de personas y correos reenviados. Un sandbox de detonación puede hacer clic en un botón; no puede escribir una frase de contraseña que viajó por otro canal. Envía el enlace por correo y la frase de contraseña por un mensaje directo de Slack o por SMS.
Filtrado de bots por user agent
Descarta los bots de vista previa conocidos (Slackbot, LinkedInBot,
Twitterbot) antes de que lleguen al código que muestra el secreto.
Merece la pena como protección adicional, pero nunca basta por sí
solo: los escáneres de seguridad usan de forma habitual user agents de
navegadores reales precisamente para que las páginas maliciosas no
puedan ocultarse del análisis.
Expiración corta
Limita el periodo en que puede haber daños. Si un enlace duró quince minutos, los escáneres, los archivos de registro y el historial del chat se quedan con una URL que ya no lleva a nada.
Cache-Control: no-store
Ponlo en todas las respuestas que muestran el secreto para que ningún intermediario (proxy corporativo, CDN, caché del navegador) guarde una copia del contenido después de la única lectura legítima.
El caso concreto de las pasarelas de seguridad del correo
Las pasarelas corporativas merecen un apartado propio porque abren el enlace dos veces. Los productos de este tipo (Microsoft Defender Safe Links es el ejemplo documentado) reescriben todas las URL del correo entrante para que pasen por su propio dominio de análisis. Lo normal es que la URL se abra una vez en la entrega, para analizar su reputación, y otra vez en el momento del clic, antes de redirigir a la persona al destino real.
Si cualquiera de esas dos peticiones destruyera el secreto, el
destinatario vería siempre un enlace muerto, un error que a los
usuarios les parece aleatorio pero que es totalmente determinista. La
separación entre GET y POST absorbe las dos peticiones sin ningún
problema, porque las dos son simples GET. La redirección en el momento
del clic lleva después a la persona a la página para mostrar el
secreto, que es donde se hace el POST.
Casos límite que siguen dando problemas
- Sandboxes que ejecutan JavaScript. Algunos entornos de detonación
cargan la página e interactúan con ella. En teoría, un sandbox
agresivo podría hacer clic en un
POSTdetrás de un botón sin frase de contraseña. La frase de contraseña es la red de seguridad. - Enlace y frase de contraseña por el mismo canal. Si los dos van en un mismo correo, todas las defensas anteriores se reducen a «esperar que nadie lea ese correo». Sepáralos siempre en canales distintos.
- Reenvíos. Cada reenvío a otro chat u otro sistema de correo provoca una nueva ronda de vistas previas. Con la separación no hacen daño, pero cada salto deja otro conjunto de registros con la URL, y eso es un argumento a favor de una expiración corta.
- Lecturas simultáneas. Si dos personas hacen clic para mostrar el secreto a la vez, la lectura y la destrucción tienen que hacerse en un solo paso indivisible: exactamente una petición gana y la otra recibe «ya visto». Una implementación que las haga por separado puede entregar el contenido dos veces.
Prueba tu propia herramienta
Antes de confiar credenciales reales a cualquier servicio para compartir secretos, haz esta comprobación de dos minutos:
- Crea un secreto desechable con la destrucción tras la lectura activada.
- Ábrelo desde un terminal, que es justo lo que hace un generador de
vistas previas:
curl -s -o /dev/null -w "%{http_code}" "<url>" - Abre el enlace en un navegador. Si ves «ya visto», la herramienta
destruye el secreto con un
GETy no es seguro pegar sus enlaces en un chat o un correo. - Si ha sobrevivido, muéstralo de la forma normal en el navegador y
vuelve a hacer
curl: ahora deberías recibir una respuesta explícita de que ya no existe o se ha destruido, no el contenido.
Cómo lo implementa LinkPilot
Para que conste, este es el diseño que usan los enlaces secretos de
LinkPilot: un GET al endpoint que muestra el secreto devuelve solo
metadatos (el estado y si hace falta una frase de contraseña), y solo
se registra una vista con un POST explícito. Las frases de
contraseña se convierten en un hash SHA-256 en el cliente y tienen un
límite de cinco intentos por IP, por minuto y por secreto. Todas las
respuestas que muestran el secreto llevan Cache-Control: no-store,
los secretos destruidos o expirados devuelven HTTP 410 con el
contenido almacenado sobrescrito, y el historial de auditoría registra
cada evento con la IP convertida en hash con una sal que cambia cada
día. La IP sin procesar no se guarda nunca. Todos los detalles están
en la página de arquitectura de seguridad.
Pruébalo gratis, sin registro
Mensaje que se autodestruye
Envía una nota que desaparece después de leerse una vez.
Límites de este diseño
Con sinceridad, estos son sus límites: nada de lo anterior protege un secreto después de una lectura legítima, porque el destinatario puede copiar el contenido, hacer una captura de pantalla o reenviarlo. No sirve de nada si la frase de contraseña viaja con el enlace. Y no puede impedir que una persona que recibió el enlace por error lo abra antes que el destinatario previsto. Para eso está el historial de auditoría: ves la vista inesperada y rotas la credencial.
Lecturas relacionadas
- Cómo funcionan los enlaces secretos de un solo uso
- Por qué las URL filtran información confidencial
- Arquitectura de seguridad de LinkPilot
Referencias
- IETF, RFC 9110: HTTP Semantics, section 9.2.1 Safe Methods.
- Slack API, Unfurling links in messages.
- Microsoft Learn, Safe Links in Microsoft Defender for Office 365.
