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.

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.

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.

PropertyDesign
Bound toThat browser, and one recipient on one transfer, or one upload request
Maximum lifetime24 hours, and never past the transfer's or request's expiry
Idle timeout1 hour; a download in progress counts as activity
Sessions per browserUp to 10, one per link, so verifying a second link doesn't end the first
Ended byExpiry, 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:

LimitStarting value
Link-step requests per IP60 a minute in the service; 100 a minute at the firewall
Unknown-link answers per IP20 an hour, then every link step from that IP is refused for the rest of the hour
Failed code attempts per IP30 an hour, across all links
Codes per link1 every 30 seconds, 20 a day
Codes per email address5 an hour and 50 a day, across all tenants
Codes per tenant1,000 an hour
Attempts per code5, within 10 minutes
Failed attempts per recipient or uploader20 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