Nulliance

Nullbin

A self-hostable, zero-knowledge encrypted pastebin for text and files, designed so the server never holds anything it could leak.

Nullbin shares text and files through a private link. Everything is encrypted in the browser before upload, so the server only ever stores ciphertext it cannot read. The key lives in the part of the link after the #, which browsers never send to the server.

Pastes always expire, from five minutes to thirty days. They can be protected with a password, opened a fixed number of times before burning (one to a thousand), and carry an encrypted discussion.

The design starts from one assumption: the server, its storage, and the network may all be hostile. Every component is either kept away from plaintext entirely or reduced to the smallest job it needs to do.

Encryption

  • Each paste gets a 256-bit key from libsodium's random generator. It is carried only in the link fragment and removed from the address bar once read.
  • Content is encrypted with XChaCha20-Poly1305 through libsodium's secretstream, in authenticated frames. Any modified, reordered, duplicated, or truncated frame fails to decrypt, and a stream that ends before its final tag is rejected.
  • Filenames and MIME types are encrypted inside the first frame, and the blob header is authenticated with it, so the server sees only the total ciphertext size.
  • A password adds to the link key rather than replacing it. The final key is BLAKE2b, keyed with the link key, over Argon2id of the password and a random salt, so a password guess is useless without the link.
  • Discussions use a separate key derived from the paste key, and every comment is bound to its paste. The server can neither read nor forge comments.

What the server keeps

The server stores ciphertext and only the metadata it needs to enforce expiry and access: an ID, creation and expiry times, the burn setting and open count, the ciphertext size, and SHA-256 hashes of 256-bit capability tokens. The tokens themselves are never stored, and the upload token's hash is deleted when a paste is published.

There are no access logs. Errors are logged without paths, IDs, tokens, IP addresses, or user agents, and the proxy is configured not to record client addresses. IP addresses exist only in memory, hashed, for rate limiting.

Minimal attack surface

  • The backend is Go built only on the standard library, with no third-party modules. The frontend has two runtime dependencies, pinned exactly and installed without install scripts.
  • No database, no accounts, and no cookies. With no ambient credentials there is nothing for cross-site request forgery to use; capability tokens travel in custom headers that other origins cannot send.
  • Paste IDs are 128 random bits and must match a strict pattern before any file is touched. All file access goes through Go's os.Root, so even a bug cannot reach outside the data directory.
  • Missing, expired, burned, and wrong-token requests all return the same 404, and token hashes are compared in constant time.
  • The static frontend and the API run in separate containers. Only the API path is proxied to the API, and the API cannot write the frontend.
  • Both containers use distroless images as non-root users, with read-only root filesystems, no added capabilities, and no shell or package manager. The API container has no internet access. The proxy serves only HTTP/1.1 and HTTP/2, with its admin interface turned off.

Browser safety

  • Decrypted text is only ever written as plain text, never as HTML. The Content Security Policy requires Trusted Types with no policies, so supporting browsers refuse HTML injection outright.
  • No third-party scripts are loaded. Assets come only from the frontend origin with Subresource Integrity, and the page is cross-origin isolated.
  • Image and text previews appear only when the reader asks, and only after the whole attachment is authenticated. Images are limited to PNG, JPEG, GIF, and WebP, detected from their own bytes, never SVG. Documents and active formats are never rendered.
  • The viewer warns about invisible or direction-changing characters that can make text display differently from what it contains.

Abuse resistance

Uploads, downloads, and comments are bounded per client network and globally: a storage budget charged as bytes arrive, caps on unfinished uploads and concurrent transfers, per-client bandwidth limits, and strict timeouts against slow clients. Forwarded client addresses are trusted only from configured proxies, and the rate limiter fails closed when its table is full. Writes are atomic, and burn opens are reserved durably before any ciphertext is sent, so concurrent readers cannot exceed the open limit.

Supply chain and verification

Releases are reproducible bit for bit, which CI checks by building twice. Every third-party GitHub Action is pinned to a commit, workflows run with read-only permissions, and every push is scanned for secrets. A running instance publishes the hashes of the bundles it serves, so anyone can check it against a release.

Known limits

Browser-based encryption cannot protect against whoever serves the JavaScript: a malicious operator could serve code that leaks keys. The server still sees sizes, times, and client addresses in transit, and anyone holding a full link can open the paste. These limits are documented in the project's public threat model. The source is open under the AGPL-3.0 license.

Open NullbinView source