Tessera

Security

What is protected, how, and what is not.

This page is written to be checkable rather than reassuring. Where something is not built, it says so — a security page that lists only strengths is a marketing page.

Reporting a vulnerability

Email security@studioheino.com. Include enough detail to reproduce the issue; a proof of concept is welcome and never required. The machine-readable contact is at /.well-known/security.txt.

You will get an acknowledgement, an assessment, and — if you would like one — credit when the fix ships. Reports are welcome about this website, the desktop application, the reference server and the protocol itself.

Please do not run automated scanners against the server instance other people are using, do not access or modify data that is not yours, and do not test denial of service. Anything you can demonstrate against a copy you run yourself is better for both of us; the server is designed to be run locally with one command precisely so that is possible.

This is a small project with no bug-bounty budget. That is stated plainly rather than left to be discovered after somebody has spent a weekend on it.

This website

The site is a Go program serving pages it renders itself. It has no database and no user table: accounts belong to the collaboration server, and this site holds only a short-lived in-memory session while you are signed in. Everything you can reach without signing in is stateless, cookie-free and script-free, which removes most of the vulnerability classes that dominate web security lists rather than defending against them.

JavaScriptNone. Not "minimal" — zero script bytes are served, and the content security policy is script-src 'none'. The one interactive feature is a form that posts back and is rendered on the server.
Third-party requestsNone. No analytics, no fonts from a CDN, no tag manager, no embedded video. The policy is default-src 'none' with same-origin exceptions for the one stylesheet and the images.
CookiesNone while you are browsing — no consent banner, because nothing is stored on or read from your device. Signing in sets exactly one: a random session identifier, HttpOnly, Secure, SameSite=Lax, cleared when you close the browser.
TransportHTTPS with HSTS. The process refuses to start with a non-loopback http public URL — a checksum served over cleartext is a checksum an attacker can rewrite along with the binary it describes.
Headersframe-ancestors 'none', nosniff, a cross-origin isolation set, and a Permissions-Policy that denies every browser capability by name.
Templates and assetsCompiled into the binary. Nothing is read from disk at runtime, so the container runs with a read-only root filesystem and there is no path from a file write to a rendered page.
AccountsThe browser never receives a token. See below.
The server-check toolThe only place visitor input becomes an outbound request. See below.
LogsMethod, path, status, size, duration and client address. Never query strings, never a submitted server URL as typed.
ContainerDistroless, non-root, read-only root filesystem, all Linux capabilities dropped.

Signing in, specifically

The conventional way to put accounts on a website is to let page script call the API with CORS and keep the tokens in the browser. That was rejected. Tokens in a browser have to live somewhere, and everywhere available — local storage, a readable cookie — is readable by any script that gets injected. It would also make this public marketing origin a credential-handling origin, so an XSS bug in a paragraph of prose on the download page would become an account takeover.

Instead the browser talks only to this site, over ordinary form posts, and this site talks to the collaboration server. What your browser holds is a random session identifier in an HttpOnly cookie; the access and refresh tokens stay in this server's memory and are never written to disk. Concretely:

  • No token ever reaches your browser. There is nothing for a script to steal, and there is no script — script-src is still 'none' on the sign-in page.
  • The cookie carries the __Host- prefix, which is a browser-enforced guarantee that it must be Secure, must be scoped to the whole origin, and must have no Domain — so no subdomain and no cleartext page can set or overwrite it.
  • Every state-changing form carries a CSRF token bound to the session and compared in constant time, on top of the SameSite=Lax cookie and the cross-origin check that applies to every POST on the site.
  • Sign-out revokes the session at the API, not just locally, so the refresh token stops working immediately rather than at its natural expiry.
  • Refresh rotation is serialised per session. tessera/1 refresh tokens are single-use, and presenting one twice makes the server revoke the whole sign-in as a suspected theft. Two concurrent requests from one browser would otherwise do exactly that, so only one rotation is ever in flight for a session.
  • Sessions live in memory and do not survive a restart. Being asked to sign in again after a deploy is the price of never writing a credential to disk.
  • Accounts cannot be enabled over cleartext. If the collaboration server were configured on plain http off loopback, the site refuses to start rather than forward your password across a network in the clear.

The password itself unavoidably passes through this server on its way from the form to the API. It is never logged, never stored, never placed in a URL, and never echoed back into a page — a rejected sign-in re-renders with the email address filled in and the password box empty.

The server-check tool, specifically

"Paste a URL and we will check it" is, implemented carelessly, a server-side request forgery primitive: it lets a stranger make this host connect to addresses they cannot reach themselves and reports what came back. Four things prevent that, layered because the first three each have a known bypass on their own.

  1. The submitted URL is reduced to scheme, host and port. Path, query, fragment and credentials are discarded, so the probe can only ever request one fixed path — /.well-known/tessera — and whatever else the target serves is unreachable through this site by construction rather than by filtering.
  2. Every address the name resolves to is checked against the blocked ranges — loopback, RFC 1918, carrier-grade NAT, link-local (which is where cloud metadata services live), multicast, and their IPv6 equivalents including NAT64 and 6to4 addresses with an IPv4 address embedded in them.
  3. That check is not the boundary. The boundary is a hook in the network dialler that runs with the concrete address the kernel is about to connect to, on every attempt. A name that resolved to a public address on the first lookup and a private one on the second — DNS rebinding — is refused there.
  4. Redirects are not followed. A 302 pointing at a link-local address would otherwise walk past every check above, because the redirect target never went through the first one.

On top of that, the request is deadline-bounded, the response is size-capped and parsed into a fixed structure, refusals are worded identically for every blocked range so the tool cannot be used as a network mapper one query at a time, and the endpoint has its own rate budget separate from and much tighter than the rest of the site.

The application

Your passwordNever stored, in any form, anywhere on disk. It is exchanged for tokens once and dropped.
Access tokenMemory only, never written, never logged, fifteen-minute lifetime.
Refresh tokenEncrypted at rest with the Windows data-protection API, bound to your Windows user account and to that machine, with an application-specific entropy value mixed in. On a platform with no OS-backed encryption the store refuses to save rather than falling back to plaintext.
TransportThe client refuses plain http to any host that is not 127.0.0.1, localhost or ::1, so credentials cannot be sent in the clear by misconfiguration.
Socket authenticationThe token travels in the WebSocket sub-protocol list rather than the URL, so it stays out of proxy logs.
Your notesPlain JSON files in your user profile, written atomically. They are not encrypted at rest — full-disk encryption is the right layer for that, and pretending otherwise would be a false claim.

Stated rather than implied: the entropy mixed into the credential encryption is compiled into the binary. That makes it obfuscation against unrelated software on the same machine, not a boundary against malware already running as you. Nothing available to a desktop application defends against that, and claiming otherwise would be worse than saying so.

The reference server

PasswordsArgon2id with OWASP-profile parameters, stored in the hash so they can be raised later without invalidating anyone.
LoginOne indistinguishable failure for "no account", "wrong password" and "temporarily locked", with a dummy hash computed when no account matched so response time is not an enumeration oracle.
SessionsRefresh tokens are opaque, hashed at rest, single-use and rotating. Presenting one twice revokes the whole session.
AuthorisationChecked per message, server-side, on the listing, the read, the socket handshake and every write. Non-members are answered "no such document" rather than "permission denied", because the second answer confirms the document exists.
UploadsVerified against the claimed content hash while streaming; reads gated by membership; HEAD answers 404 to anyone who could not GET, so it is not an existence oracle.
Rate limitsEnforced server-side regardless of what a client throttles itself to.

What is not built

These are known, deliberate and worth deciding about before you rely on Tessera for something sensitive.

  • No end-to-end encryption. A server can read the documents published to it. The message envelope is designed so E2E remains possible, but the mode that would need is not implemented. Privacy from the server comes from owning the server.
  • The installer is not code-signed. Windows SmartScreen will warn. Verify the published SHA-256 instead; that check is the one that means something.
  • No audit log of membership changes on the server.
  • An access token stays valid for its remaining lifetime — at most fifteen minutes — if the server's database is unreachable at check time.
  • Per-address rate limits are per process. A distributed attempt needs limiting at the edge as well.
  • Notes are not encrypted on your disk. Use full-disk encryption.
  • No crash reporting. Good for privacy, bad for finding out that something broke. It is on the list, and when it arrives it will be opt-in and this page will change.
  • No password reset and no email verification. The server has no mail capability, so there is no link to send. A lost password is recovered by contacting us and establishing ownership some other way. This is the largest gap on this page and the one being closed first.
  • No two-factor authentication. A password is currently the only factor.