If your one-time link is consumed on GET, it dies the moment Slack
unfurls it. Here's why it happens, which systems fetch your links
without asking, and how to design around all of them.
The default failure mode
Most chat and email clients try to render a preview when they see a URL. The flow looks like this:
- User pastes the secret link into Slack.
- Slack's unfurler fetches the URL (issues
GET). - Your reveal handler treats the request like a human view, burns the secret, and returns the payload.
- Slack receives the burned payload (or a generic page) and renders it.
- The human clicks the link, sees "already viewed," and pings the sender for a new one.
The unfurler had no malice. Your handler conflated "someone fetched the URL" with "the recipient read the secret." Those are different events, and the entire design problem is keeping them apart.
There's a worse variant hiding in step 3: if the unfurler's GET
received the actual payload, the secret now sits inside a third
party's preview-rendering infrastructure. Best case, it was
discarded after the preview was built — but you have no way to
confirm that, and no audit event telling you it happened.
Who actually fetches your links
"Slack unfurls previews" understates the problem. A URL pasted into a modern workplace is typically fetched by several systems before any human clicks it:
| Fetcher | When it fires | Identifiable? |
|---|---|---|
| Chat unfurlers (Slack, Teams, iMessage, WhatsApp, LinkedIn) | The moment the message is sent | Usually — distinct user agents such as Slackbot-LinkExpanding |
| Email security gateways (Mimecast, Proofpoint, Microsoft Defender) | On delivery, and often again at click time | Often not — many present real browser user agents |
| Endpoint protection / antivirus | When the URL reaches the recipient's machine | Rarely |
| Corporate web proxies | At click time, before the user's request is forwarded | Rarely |
| Link-preview browser extensions | On hover or page load | No — they ride the user's own browser session |
Vendor behavior in this table is a moving target — treat specific
gateway claims as "as published July 2026, verify against current
docs." The common thread doesn't move, though: every one of these
systems issues a plain GET, because that's the only safe,
universal way to look at a URL you don't control.
The fix: separate fetch from reveal
The HTTP semantics are already there. RFC 9110 defines GET as safe
and idempotent — a request anyone may issue freely because it isn't
supposed to change anything. POST is the action verb. So treat the
reveal as a POST action behind a visible button:
GET /s/abc123 → HTML page: "Click to reveal this secret"
POST /s/abc123 → burns the secret, returns the payload
Now Slack's GET returns a neutral metadata page. The secret only
dies when the human clicks the button. Any scanner that respects
HTTP semantics — nearly all of them, since issuing side-effectful
POSTs against arbitrary URLs would break half the web — never
touches the payload at all.
A worked trace
Follow one link end to end. You create a secret with burn-after-read and a passphrase, then paste the URL into a Slack DM and an email.
- Slack's unfurler issues
GET /s/abc123. Response: a small metadata page — status active, passphrase required. No view recorded. Secret alive. - The recipient's email gateway rewrites the URL in the email
and fetches it at delivery time for reputation analysis. Same
GET, same metadata. Still alive. - The recipient clicks the emailed link. The gateway's
click-time scanner fetches once more (
GET), then redirects the browser onward. - The browser loads the reveal page — another
GET. The page shows a reveal button and a passphrase field. Still no view recorded. - The recipient enters the passphrase and clicks reveal. The
browser issues
POST /s/abc123. The view is recorded, the payload is returned, the secret burns. - Anyone fetching the URL afterward gets an explicit "already viewed and destroyed" response instead of content.
Five fetches, one reveal. That ratio is normal in a corporate environment — any design that can't tolerate it will fail on day one, and the sender will just paste the password into chat instead.
Layered defenses
The GET / POST split
Handles every well-behaved unfurler and scanner, which is the overwhelming majority of automated traffic. This is the foundation; everything below is depth.
Passphrase gate
Covers what the split can't: sandboxes that execute JavaScript and interact with the page, plus accidental human clicks and forwarded emails. A detonation sandbox can click a reveal button; it cannot supply a passphrase that traveled through a different channel. Send the link by email and the passphrase by Slack DM or SMS.
Bot user-agent filtering
Drop known preview bots (Slackbot, LinkedInBot, Twitterbot)
before they reach any reveal logic. Worth doing as belt and braces,
but never sufficient alone: security scanners routinely present
real browser user agents precisely so malicious pages can't cloak
themselves from analysis.
Short expiry
Bound the damage window. A link that lived for fifteen minutes leaves scanners, log files, and chat history holding a URL that no longer resolves to anything.
Cache-Control: no-store
Set it on every reveal response so no intermediary — corporate proxy, CDN, browser cache — retains a copy of the payload after the one legitimate read.
Email security gateways specifically
Corporate gateways deserve their own section because they fetch twice. Products in this class rewrite every URL in inbound mail through their own scanning domain. The URL is typically fetched once at delivery for reputation analysis, and again at click time, before the human is redirected to the real destination.
If either fetch burned the secret, the recipient would always see
a dead link — a failure mode that looks random to users but is
completely deterministic. The GET/POST split absorbs both
fetches for free, since both are plain GETs. The click-time
redirect then lands the human on the reveal page, where the POST
happens.
Edge cases that still bite
- JavaScript-executing sandboxes. Some detonation environments
render the page and interact with it. A
POSTbehind a button without a passphrase could, in principle, be clicked by an aggressive sandbox. The passphrase gate is the backstop. - Link and passphrase in the same channel. If both travel in one email, every defense above collapses to "hope nobody reads that email." Always split channels.
- Forwarding. Every forward into a new chat or mail system triggers a fresh round of unfurls. Harmless with the split — but each hop is another set of logs holding the URL, which is an argument for short expiry.
- Concurrent reveals. If two people click reveal simultaneously, the burn must be atomic: exactly one request wins and the other gets "already viewed." A non-atomic implementation can serve the payload twice.
Test your own tool
Before trusting any secret-sharing service with real credentials, run this two-minute check:
- Create a throwaway secret with burn-after-read enabled.
- Fetch it from a terminal — this is exactly what an unfurler does:
curl -s -o /dev/null -w "%{http_code}" "<url>" - Open the link in a browser. If you see "already viewed," the tool
burns on
GETand is not safe to paste into chat or email. - If it survived, reveal it properly in the browser, then
curlagain — you should now get an explicit gone/burned response, not the payload.
How LinkPilot implements this
For the record, this is the design LinkPilot's Secret Links use: a
GET on the reveal endpoint returns metadata only (status, and
whether a passphrase is required); a view is recorded only on an
explicit POST reveal. Passphrases are SHA-256 hashed client-side
and rate-limited to five attempts per IP per minute per secret.
Every reveal response carries Cache-Control: no-store, burned or
expired secrets return HTTP 410 with the stored payload overwritten,
and the audit timeline records each event with a hashed IP under a
daily-rotating salt — the raw IP is never stored. Full details live
on the security architecture page.
Try it free, no signup
Burn-After-Read Secret
Share a message that destroys itself after one view.
Limits of this design
Honest boundaries: none of the above protects a secret after a legitimate reveal — the recipient can copy, screenshot, or forward the payload itself. It doesn't help if the passphrase travels with the link. And it can't stop a human who was mistakenly sent the link from opening it before the intended recipient does — that's what the audit trail is for: you see the unexpected view, and you rotate the credential.