Secure Link Sharing Best Practices: Protect Your Download URLs
Best practices for sharing secure download links. Learn about URL token security, link expiry, download limits, and preventing unauthorized link forwarding.
A secure download link treats the URL itself as part of the key. The slug after the slash should be at least 128 bits of entropy (think hexatransfer.com/d/7Kj9xQmN2vP8rBwLsE4fT), expire in hours rather than days, cap downloads at one or two attempts, and — critically — never be the only secret. Pair it with a password-derived AES-256-GCM key that lives in the URL fragment (after the #) so it never hits the server log, and you've defeated the most common link-leak vectors from Slack previews to corporate proxy archives.
Entropy: the math of guessability
A 6-character slug made of letters and digits has 36^6 = 2.1 billion combinations. Sounds huge until you realize a botnet can test 100,000 URLs per second against a CDN — meaning an unauthenticated attacker enumerates the full space in roughly six hours. Bump to 128 bits (22 characters of base64url) and the guess time exceeds the heat death of the sun. Use crypto.getRandomValues(), never Math.random(), and never encode a database ID. Sequential tokens leak transfer volume to anyone who collects two samples a week apart.
Put the key in the fragment, not the path
Everything after # in a URL is the fragment identifier — browsers don't send it to the server. Storing the decryption key there is the pattern Firefox Send pioneered: /#k=abc123xyz. The server sees only the opaque download slug, so its access logs, SIEM exports, and breach dumps contain no key material. CDN edge caches can't decrypt the payload either. Slack unfurls drop the fragment when they probe the URL, so the preview bot never sees the key. This single trick blocks 80% of practical link-interception attacks.
Expiry windows that match the threat
Default 7-day expiry is too generous for most use cases. Calibrate to workflow: a 15-minute window for a wire-transfer .pdf to a bank, 4 hours for a contract review, 24 hours for a photo album, 7 days only for async international collaboration. Express expiry as an absolute UTC timestamp in the signed URL payload and enforce server-side — never trust a client-side clock. At expiry, overwrite the ciphertext on disk with zeros (ideally via blkdiscard on NVMe) so a later disk forensics pass finds nothing.
Download counters that actually work
A single-download link sounds airtight until you realize browsers aggressively retry partial transfers. If Chrome drops at 80% and resumes with a new Range request, does that count as two downloads? Implement the counter at the file-completion boundary, not per HTTP request. Increment only when the final byte ships and the auth tag verifies. Alternatively, track unique session IDs over a 10-minute window: one session = one download, regardless of how many Range requests it issues. WeTransfer conflates these and has been criticized for falsely marking links as consumed.
Signed URLs with short-lived tokens
For enterprise flows, wrap the download URL with an HMAC-SHA256 signature over (slug, expiry, max_downloads, issuer_id). The server validates the signature before serving any byte. S3 pre-signed URLs do this natively; R2 and Backblaze B2 follow the same pattern. A signed URL that leaks to Slack is still dangerous, but the short expiry (say, 5 minutes) limits damage. Pair this with IP binding — the signature includes the expected /24 prefix — to thwart link forwarding across geographies.
Preventing referrer and preview leaks
Browsers send the full URL (minus fragment) in the Referer header unless you set Referrer-Policy: no-referrer on the download page. Without that header, clicking an external link from the download page leaks the slug to every tracker on the destination. Also set X-Robots-Tag: noindex, nofollow and a robots.txt that blocks /d/, so Googlebot doesn't archive a link that got accidentally pasted on a forum. For Slack and Teams previews, return a 204 No Content to User-Agents matching their unfurl bots.
Forwarding controls and recipient binding
Once Alice sends Bob a link, Bob can forward it to anyone. To prevent that, bind the link to Bob's identity. Options in increasing strength: email verification (one-time token to bob@firm.com), SMS verification (OTP to +1-415 number), passkey registration on first visit, or OIDC login via Bob's Google Workspace. Each step trades friction for containment. For a GDPR Article 15 subject-access response, email verification is usually sufficient. For a HIPAA-regulated lab result, passkey or OIDC belongs.
Revocation on demand
Even with all guards in place, something will go wrong — a laptop stolen, a contractor terminated, a wrong recipient. A revoke button that kills the link instantly matters. Implementation: maintain a revocation list keyed on slug hash, checked on every download. Propagate to CDN edges within 60 seconds via purge API (Cloudflare's /zones/:id/purge_cache takes a URL list). HexaTransfer exposes revocation through the sender's dashboard; Dropbox Transfer and Smash both support it but charge extra. Test revocation quarterly — dead buttons during incidents are worse than no button.
Telemetry without surveillance
Log enough to detect abuse (IP hash, User-Agent family, timestamp, bytes served) but not enough to reconstruct recipient identity. Hash IPs with a daily-rotating key so correlation windows stay short. Never log the URL fragment, obviously. A 30-day retention window satisfies SOC 2 Common Criteria 7.2 without creating a surveillance archive. Expose abuse counters to the sender in the dashboard so they see "3 failed password attempts from AS15169" and know to revoke.
Try it at hexatransfer.com — free, no account, 10 GB max.
Send large files securely with end-to-end encryption
Transfer files up to 10 GB for free with end-to-end encryption. No account required. Your files are encrypted in your browser before upload — no one else can read them.
Send a file