Security

    Link Scanners and One-Time Secrets

    How URL unfurlers and security scanners can accidentally burn one-time secrets — and the patterns that defend against it.

    By W. Miller · May 26, 2026 · 8 min read

    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:

    1. User pastes the secret link into Slack.
    2. Slack's unfurler fetches the URL (issues GET).
    3. Your reveal handler treats the request like a human view, burns the secret, and returns the payload.
    4. Slack receives the burned payload (or a generic page) and renders it.
    5. 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.

    "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.

    1. Slack's unfurler issues GET /s/abc123. Response: a small metadata page — status active, passphrase required. No view recorded. Secret alive.
    2. 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.
    3. The recipient clicks the emailed link. The gateway's click-time scanner fetches once more (GET), then redirects the browser onward.
    4. The browser loads the reveal page — another GET. The page shows a reveal button and a passphrase field. Still no view recorded.
    5. 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.
    6. 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 POST behind 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:

    1. Create a throwaway secret with burn-after-read enabled.
    2. Fetch it from a terminal — this is exactly what an unfurler does: curl -s -o /dev/null -w "%{http_code}" "<url>"
    3. Open the link in a browser. If you see "already viewed," the tool burns on GET and is not safe to paste into chat or email.
    4. If it survived, reveal it properly in the browser, then curl again — 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.

    Open tool

    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.

    Tools mentioned in this article

    Frequently asked questions

    Run smarter links with LinkPilot

    Tracking, UTMs, QR codes, AI insights, and white-label reporting — in one workspace. Free to start.

    Create your free account

    Read next