Recipient verification
One-time codes, verified sessions, lockouts and why a forwarded link gets nothing.
Before a recipient or uploader sees a single file name, message or byte, they prove they control the invited email address at that moment. A link on its own grants nothing, so a forwarded email, a leaked link or a link scanned by a mail filter can't be used to reach the files.
Why links alone aren't enough
A plain shared link is a bearer token: whoever holds it gets in, and links get forwarded, pasted into chats and fetched by email scanners. Secure File Transfer treats the link as an address, not a key. The key is a one-time code sent to the invited mailbox, and using it creates a session that lives only in the browser that verified.
The link
Each recipient of a transfer, and each upload request, gets its own link with a 256-bit random token.
- The token travels in the URL fragment, the part after
#, which browsers never send to a server. It never reaches the CDN, the firewall, the load balancer or any log. - The page removes it at once from the address bar and history, then posts it to the API in a request body.
- The service stores only a hash of the token, never the token itself.
- A token reveals almost nothing. On its own it returns only the kind of link, its access state and its language. No file names, no sender message, no sizes.
- It is not single use. A recipient may come back over several days, so the link stays valid until the transfer or request ends.
Email security scanners that open the link get the static page. Looking a link up changes nothing, so a scanner uses up no code and no attempt.
One-time codes
- The recipient asks for a code. It is always sent to the address stored with the recipient or request, never to an address given in the request, so nobody can redirect it.
- The code has 6 digits from a secure random generator. It is stored only as a keyed hash, and the code email contains no link.
- A code is valid for 10 minutes and allows 5 attempts. Requesting a new code supersedes every older one, so only the newest can match.
- A code is bound to its recipient or request. A code for one recipient can't open another recipient's link, even on the same transfer.
Every attempt, right or wrong, is written to the audit log with its source IP and user agent.
Verified sessions
A correct code creates a verified session with its own fresh random token, held in a secure, HttpOnly cookie.
| Property | Design |
|---|---|
| Bound to | That browser, and one recipient on one transfer, or one upload request |
| Maximum lifetime | 24 hours, and never past the transfer's or request's expiry |
| Idle timeout | 1 hour; a download in progress counts as activity |
| Sessions per browser | Up to 10, one per link, so verifying a second link doesn't end the first |
| Ended by | Expiry, sign-out, or revocation of the transfer or request, which ends its sessions at once |
The session is not tied to an IP address, because mobile and corporate networks change addresses mid-session; the source IP goes into every audit record instead.
Why forwarding gives nothing
Each layer is bound to the one before it:
- The link gets a forwarded reader only as far as "verification required".
- The code goes only to the invited mailbox.
- The session lives in the cookie of the browser that verified.
- Each download runs under a download grant that belongs to that session. Opening a forwarded download address without the cookie fails, and with another recipient's cookie it fails too.
Someone who receives a forwarded email and doesn't control the original mailbox can't get past the first step.
Lockouts and unlocks
After 20 failed attempts in total for one recipient on one transfer, or for one upload request, that recipient or uploader is locked. A locked subject is refused before any code is checked, and no further code emails are sent.
The member who created the transfer or request, and every tenant admin, are emailed, and the web app shows the recipient as locked. The owner or a tenant admin unlocks them. Unlocking always issues a new link and the old one stops working, so whoever caused the lockout with a leaked link is cut off. API keys can't unlock anyone: a lockout may signal an attack, so a person decides.
These limits together mean an attacker holding a leaked link gets at most 20 guesses before a person has to act: about a 1 in 50,000 chance against a 6-digit code.
Rate limits
The link steps (looking up a link, requesting a code, entering a code) carry their own starting limits:
| Limit | Starting value |
|---|---|
| Link-step requests per IP | 60 a minute in the service; 100 a minute at the firewall |
| Unknown-link answers per IP | 20 an hour, then every link step from that IP is refused for the rest of the hour |
| Failed code attempts per IP | 30 an hour, across all links |
| Codes per link | 1 every 30 seconds, 20 a day |
| Codes per email address | 5 an hour and 50 a day, across all tenants |
| Codes per tenant | 1,000 an hour |
| Attempts per code | 5, within 10 minutes |
| Failed attempts per recipient or uploader | 20 in total, then locked until unlocked |
The per-address caps also stop someone using a leaked link to flood the real recipient's inbox with codes. Limits change only through deploy configuration, for every tenant at once.
If the store that holds the rate-limit counters is unavailable, link verification fails closed: it refuses rather than skipping the checks. The counts that decide access, such as code attempts and lockouts, live in PostgreSQL, so they survive a failover.
Source: PRD, Authentication and authorization, Abuse and rate limits, Data model