title: Security model description: What the server can see, the one deliberate exception, and the deployment requirements. navigation: icon: i-lucide-shield #
What the server stores #
Ciphertext, an initialisation vector, and metadata: kind, size, expiry, read counter, and whether a password is required. That is all. It cannot decrypt a paste, and neither can anyone holding a database dump.
The decryption key is generated in the browser and travels in the URL fragment, which browsers never transmit. See the introduction for the mechanics.
The one exception: email sharing #
Sharing a paste by email is the single place where the decryption key reaches the server, and it is worth being explicit about.
A usable link necessarily contains the key. For the server to compose and send that email, the browser has to hand it over. This happens:
- only at creation time, in the same request that creates the paste,
- only for signed-in users,
- never for an existing paste — no endpoint accepts a key for a paste that already exists, which is what bounds the exposure.
The key is held for the lifetime of that one request. It is written to no column and to no log; the recipients table records addresses and delivery status only.
::note If that tradeoff doesn't suit your instance, don't use the feature. The Share by email button on the result screen opens your own mail client with the link pre-filled and never involves the server. It is available to everyone, signed in or not, even when no mail provider is configured. ::
One message goes out per share, with every recipient in blind copy — recipients never learn about
each other, and the sender is in To so they can see who shared it with them.
Abuse protection #
Independent layers, all active by default:
- Cloudflare Turnstile on paste creation, required for every tier.
- Rate limiting on creation, per IP for anonymous and per account for signed-in users, with a stricter separate cap for uploads.
- Instance quotas on total pastes and stored bytes, evaluated live.
- Size validation server-side, never trusting a client-declared size.
- Automatic IP banning for requests to known probe paths (
wp-admin,.git,phpinfo.php, …) or from bots identified as untrusted, with an admin-managed allowlist and blocklist. - Atomic read-counter decrement, so concurrent requests cannot over-read a one-read link.
- A Content-Security-Policy limiting outbound requests to the instance itself and Turnstile,
alongside
Referrer-Policy: no-referrerso the paste id never leaves in aReferer.
::note The CSP is the layer that matters most in a zero-knowledge design: an XSS here does not leak a session, it leaks the decryption key of every paste opened while it runs. The policy still allows inline script, so it contains exfiltration rather than preventing injection. ::
Accounts #
Three roles:
| user | admin | super admin | |
|---|---|---|---|
| Manage own pastes | ✅ | ✅ | ✅ |
| Change settings, view stats | ❌ | ✅ | ✅ |
Create and delete user accounts |
❌ | ✅ | ✅ |
Act on admin / super admin accounts |
❌ | ❌ | ✅ |
| Change anyone's role | ❌ | ❌ | ✅ |
Nobody can delete or demote themselves, whatever their role. A super admin can rotate another super admin out, which is the intended way to hand the instance over.
Two-factor authentication is TOTP (RFC 6238) with single-use backup codes. It is opt-in by default,
including for the super admin, so that evaluating an instance isn't a chore. require_2fa enforces
it instance-wide.
Deployment requirements #
::warning
Set TRUSTED_PROXY_DEPTH to match your actual setup. It defaults to 0, which ignores
X-Forwarded-For entirely and uses the connection's own address — right when nothing sits in front,
and safe when something does. A value larger than your real chain lets a caller write their own
address into the header, and with it get another address banned.
::
- Serve over HTTPS. The fragment key is in the URL — without TLS it is exposed in transit.
- Set a strong
BETTER_AUTH_SECRETand keep it stable. Changing it invalidates every session. - Back up your database. Expired pastes are purged hourly and are not recoverable; that is the point, but it also means a backup is your only safety net for the rest.
Data protection #
DELETE /api/account/me, exposed at /account, deletes the account with a full cascade: pastes,
their files, email recipient records, sessions and statistics. Entries in the admin audit log are
kept but anonymised — the action stays on record, the actor does not.
It requires the account password and retyping the account's own email address.
::note The super admin account cannot delete itself. It is a system account, deliberately outside the individual right to erasure. Transfer the role to somebody else first, then delete the account. ::
IP addresses in the allowlist and blocklist are stored in clear, as operational security data — an administrator has to be able to read and manage them. Mention this in your instance's privacy policy.
Known limits #
- Anyone with the full link can read the paste. That is the point, and it means the link should be treated as the secret itself.
- No protection against a malicious operator. A modified server could serve altered JavaScript that exfiltrates keys. Self-host the instance you trust.
Reporting a vulnerability #
Open a private security advisory on the repository rather than a public issue.