binthere is a zero-knowledge pastebin: paste content is encrypted and decrypted only in
the browser, and the decryption secret never leaves the client in normal operation. This
document is the authoritative statement of what binthere does and does not protect. Read it
alongside SPEC.md (the frozen cryptographic protocol).
Treat binthere as a security-sensitive cryptographic application, not a normal web app.
- Confidentiality of plaintext from the storage/server layer. The server, the KV store,
the Durable Object, and Cloudflare's infrastructure store only ciphertext and non-secret
metadata. They cannot read paste plaintext, because the decryption secret (
F, the URL fragment) is never transmitted to them. - Integrity / authenticity of ciphertext and its security-relevant metadata. AES-256-GCM
authenticates the ciphertext, and the canonical AAD (
SPEC.md§4) binds every field that affects decryption, rendering, compression, or burn semantics. Tampering fails closed. - Password gating that is independent of the URL secret. For password-protected pastes,
neither the URL fragment secret alone nor the password alone can decrypt (
SPEC.md§2). - Strict single-consumer burn-after-read. A burn paste is delivered to exactly one reader;
concurrent or later reads get
410 Gone(SPEC.md§8). A password on a burn paste is verified against a non-consuming metadata peek before the single destructive read, so a wrong or missing password never burns the note. The peek returns the wrapped key but never the ciphertext; the trade-off (offline password guessing for someone who already holds the URL secret) is documented inSPEC.md§8 — use a strong password. - No client-side key exfiltration via the app's own code. A strict CSP, self-hosted assets, DOM-construction-only rendering, and a raw-HTML-free Markdown renderer prevent the application from turning attacker-controlled paste content into script execution — which, in a zero-knowledge app, would be equivalent to leaking the key (see §4).
binthere does not provide, and does not claim:
- Anonymity or metadata privacy. See §3.
- Protection against a compromised or malicious deployment. See §4.
- Protection of a secret you disclose. Anyone with the full URL (id and fragment) — and
the password, if set — can read the paste. Sharing the link shares the content. Fragments may
be retained in browser history, referrer chains (mitigated by
Referrer-Policy), chat-app link previews, etc. This is inherent to URL-fragment key delivery. - Guaranteed deletion from all layers/backups. Expiry and burn remove data from the live store; operational copies/logs are outside this boundary.
- Denial-of-service protection. Rate limiting is best-effort abuse mitigation (§6), not a DoS defense.
- Forward secrecy, deniability, or post-compromise recovery of individual pastes.
The zero-knowledge property covers plaintext only. binthere is not anonymous and not metadata-free. The service and Cloudflare can still observe, and may log:
- Client IP addresses and approximate geolocation.
- Timestamps of creation, reads, and deletion.
- Paste IDs (they are the storage keys and appear in request paths).
- Ciphertext size (an upper bound on, and correlate of, plaintext size).
- Expiry and burn-after-read flags and lifecycle events.
- Access patterns (how often / from where a paste is fetched).
- User-Agent and other standard request metadata.
The delete token is deliberately kept out of this metadata: it is sent in the
X-Delete-Token request header, never in the URL, so it does not appear in logged request
URLs; the server stores and compares only its SHA-256 (SPEC.md §7, §10). The decryption
secret F never appears in any request — URL, header, or body.
If you need anonymity or traffic-analysis resistance, use additional tooling (e.g. Tor); it is outside binthere's scope.
Because the decryption key is in location.hash, any script running on the page can read the
key and the decrypted plaintext. A cross-site-scripting bug is therefore equivalent to a full
key/plaintext compromise. binthere treats XSS as a top-severity class and defends in depth:
- Strict Content-Security-Policy on every static-asset response (
public/_headers):Nodefault-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; font-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; object-src 'none'unsafe-inline, nounsafe-eval, no third-party origins. This CSP governs the HTML document and every asset the browser executes; the JSON/api/*responses (which the browser never renders as a document) instead carrycache-control: no-storeandx-content-type-options: nosniff(src/index.js). - No third-party JavaScript, no CDN scripts, no analytics. All scripts, styles, and fonts are first-party and self-hosted.
- Decrypted content is rendered by DOM construction only (
document.createElement+textContent). Decrypted user content is never assigned toinnerHTML. - Markdown supports a safe subset with no raw HTML. Link
hrefs are restricted to anhttp/https/mailtoscheme allowlist;javascript:,data:, and unknown schemes are dropped. A dedicated adversarial test suite (test/markdown.test.js) exercises<script>,onerror,javascript:URLs,data:images, and raw-HTML injection. - No dangerous sinks: no
eval,new Function,document.write, inline event handlers,javascript:URLs, or dynamic<script>creation anywhere in the codebase.
binthere's client-side encryption protects plaintext from the storage/server layer, but the browser still downloads and trusts JavaScript from the server. An attacker who can modify what the server serves (a compromised deployment, a malicious operator, a supply-chain compromise, or a TLS/CDN MITM) could serve malicious JavaScript that reads the fragment key and the decrypted plaintext. Zero-knowledge server storage does not defend against a compromised delivery of the application itself. This is a fundamental limitation of all in-browser end-to-end encryption delivered over the web, binthere included. Mitigations (HTTPS-only, strict CSP, minimal first-party surface, no third-party code) reduce but cannot eliminate this trust.
The official CLI (cli/, npm package binthere) is a second client of the same frozen
protocol: it runs the vendored copies of the shared crypto/format modules in Node ≥ 20 and
is tested against the same SPEC.md §11 vectors. The zero-knowledge boundary is identical —
encryption is local and the fragment secret is never sent. Differences from the browser
client worth knowing:
- The §4 deployment-compromise limitation does not apply in the same way: the CLI's code is installed from npm and pinned locally, not re-downloaded from the paste server on every use. Its trust anchor is the npm supply chain instead (mitigated by zero runtime dependencies).
- Share URLs passed as command-line arguments are visible to other local processes and may
enter shell history;
binthere get -(URL on stdin) avoids this. Passwords and delete tokens are never accepted as flag values — hidden prompt or environment variable only.
| Boundary | Trusted with plaintext? | Notes |
|---|---|---|
| The user's browser + the served JS | Yes (unavoidable) | See the deployment-compromise limitation, §4. |
| The CLI process + its installed code | Yes (unavoidable) | Installed from npm, not served by the paste server — see "The CLI client", §4. |
| Network in transit | No | TLS protects transport; the fragment is never sent regardless. |
| Cloudflare Worker / edge | No (plaintext) | Sees ciphertext + metadata (§3). |
KV store / BurnPaste DO |
No (plaintext) | Stores ciphertext + SHA-256(deleteToken) + metadata. |
| Anyone holding the full URL (+password) | Yes | By design — that is the capability being shared. |
Paste creation is rate-limited using Cloudflare's native Workers Rate Limiting binding, keyed
by client IP. This is abuse mitigation, not authentication, and is fail-open: if the
limiter is unavailable, requests are allowed rather than blocked. Limits are documented in
SPEC.md/wrangler.toml.
- No CORS headers, deliberately. The API never sends
Access-Control-Allow-Origin, so browsers on other origins cannot read API responses. Both official clients need no CORS: the web app is same-origin and the CLI is not subject to the browser's CORS model. Adding CORS would only widen the abuse surface (third-party pages driving reads/creates from a visitor's browser and network). POST /api/pasterequiresContent-Type: application/json(else415). A cross-originapplication/jsonPOST is not a CORS "simple request", so the browser sends a preflight — which fails without CORS headers. A hostile page therefore cannot spend a visitor's creation quota (or create pastes from their IP) with a no-preflighttext/plainPOST. Both official clients already send the correct header.- Burn consumption is never a simple request.
GETon a burn id only ever returns the non-consuming head; the single destructive read isPOST /api/paste/:id/consumewith the customX-Burn-Intent: consumeheader (CORS non-simple ⇒ cross-origin preflight fails), andSec-Fetch-Site: cross-sitesenders are rejected with403. Merely knowing a burn id — via an<img>tag, a prefetching proxy, or a link-scanning bot — grants no power to destroy the note. - The request body is read under a hard cap, incrementally.
POST /api/pastestreams the body and aborts as soon as the running byte count exceedsMAX_BODY(4 MiB), so a client cannot force the Worker to buffer an oversized payload by omitting or lying aboutContent-Length(chunked uploads carry no length). At most one chunk beyond the cap is ever held before the stream is cancelled and a413returned. TheContent-Lengthheader, when present, is used only as an honest-client fast path — never as the authoritative limit. This is resource-abuse mitigation, not DoS protection (§2): Cloudflare's edge still enforces a much larger account-plan body cap ahead of the Worker.
See SPEC.md for exact byte-level details. In brief: per-paste random 256-bit CEK; AES-256-GCM
with fresh 96-bit IVs (never reused per key); password stretched with PBKDF2-HMAC-SHA256
(310 000 iterations, versioned); high-entropy fragment secret combined with the stretched
password via HKDF-SHA256 to wrap the CEK; canonical AAD binding all security-relevant metadata;
128-bit CSPRNG paste IDs; 256-bit CSPRNG delete tokens stored only as SHA-256 and compared in
constant time; decompression bounded to defend against gzip bombs. All parsing fails closed and
is prototype-pollution-safe.
This is a personal/portfolio project. Report suspected vulnerabilities privately to the
maintainer at nxfu@proton.me (do not open a public issue with
exploit details). Please include reproduction steps and affected versions. There is no
bug-bounty program. This contact is also published at
/.well-known/security.txt (RFC 9116).
Only the latest main is supported. The paste format is versioned (v); format-breaking
changes ship under a new v with an updated SPEC.md and new test vectors.