Flagship

Hall of fame

Researchers who reported a vulnerability to us, with the commits that closed it. Credited in whatever form each reporter asks for: name, handle, or anonymous, with a link if they want one. Listed without attribution until they have told us their preference; we never publish a name we have not been given.

Review notes: a mutable install ref, boot tampering, and the app browser

Design review · October 2026 · addressed in release 0.3 · credit: Jay

Jay sent us three observations from reading the code. None came with a working exploit, but each pointed at a real gap, and two of them changed what release 0.3 ships.

The signed install recipe named a branch, not a commit. The phone signs the recipe a new server installs from, but the revision it named defaulted to main, which is mutable: the signature fixed a name, not the code behind it. New servers now install a signed box release for one exact commit (a 40-hex SHA), checked before anything runs.

An app's browser grant reached past its site list. The list was checked when a tab opened or navigated, not on later clicks, form fills or reads, and every app shared one browser profile. Release 0.3 replaces it: each app gets its own browser context, every request is checked against the app's list as the browser sees it, and the current address is checked before and after every action, or the tab is closed and nothing is returned.

Physical access to a powered-off server. The boot partition is not encrypted and nothing measures it, so someone with a few minutes alone with the machine could change the early-boot code and capture the disk key at the next unlock, phone approval included. This is true today. We no longer describe phone approval or the remote lock as protection against it, and measured boot (TPM) is planned work.

Unauthenticated identity-key swap on an account with no second factor

Critical · reported and fixed October 2026 · fix 7d49a444 · credit pending the reporter’s preference

Account recovery schedules a replacement of the account’s root identity key. The endpoint that starts it verified the request against the incoming key, a self-assertion, and its only other input, the account’s current identity key, is served to anyone by the public username lookup. On an account with no second factor enrolled, that left nothing a stranger could not supply: knowing a public handle was enough to schedule a hostile key swap, which completed once the grace window elapsed.

There was also no owner veto, contrary to what our own documentation claimed. The objection endpoint is signed by the incoming key, so it is the recoverer’s own undo rather than something the displaced owner can exercise. The grace window was a notification window, not a brake.

Starting a recovery now requires an enrolled credential: a TOTP or recovery code, the short-lived proof minted by the passphrase-gated wrapped-key fetch, or a signature from the account’s currently registered key. An account with no credential cannot be recovered by anyone, which is what our naming and recovery policy always said. Finalising a recovery now requires a signature from the key being installed, where it had been an unauthenticated POST. The same pass closed a related fault we had not been told about: with its key unavailable the server fell back to a structural check on TOTP codes, so the second factor was a formality on exactly the accounts that had enrolled one.

Unauthenticated CA-signing oracle mints a relay-hub blessing for any key or host

Critical · reported and fixed October 2026 · fix 54889a4c, hardening 484ef162 and 771db538 · credit pending the reporter’s preference

The endpoint that issues relay-hub blessings did not authenticate its caller, so anyone could obtain a validly signed blessing for a key and host of their choosing: a signing oracle against the trust chain a box uses to decide whether the hub in front of it is the real one.

The issuer is now authenticated and blessings are bound to the expected apex. Two follow-ups harden the box side rather than the hole itself: a box fails closed when the hub presents no blessing and requires the expected apex, and trust lockdown now closes connections that are already open instead of only refusing new ones.

Free username rename bypassed the paid name change and the reserved-name list

Medium · reported and fixed October 2026 · fix b3d6852b, hardening c4132973 · credit pending the reporter’s preference

New accounts can only take a handle the server suggested, and choosing your own name is meant to be the paid name change; the fee is what keeps desirable names from being squatted. An old rename endpoint from early development was still live and enforced none of that. It checked only the account’s own signature, so any account holder could move to a handle of their choosing, for free, as often as they liked, including names we reserve for our own infrastructure. Because names are never taken back, every such allocation would have been permanent.

No other account’s data or keys were exposed, and a squatted reserved name could not be put to use: registering a server re-validates the name, so none could obtain DNS records or a certificate. No app ever called the endpoint, so we removed it rather than patching it. The paid name change will be a separate, new endpoint built with these checks from the start. Besides the reporter’s own test, we found one earlier rename in production, onto a reserved test-environment name; both allocations have been reverted.

A second review of the fix found two places where it relied on something else being true, and both are now closed. Our edge server forwards any API request it does not recognise to the backend, so the removed endpoint answered “not found” only because the backend lacked it too; the edge now refuses it directly, whatever its spelling, and a test proves such a request never reaches the backend or the database. And our compiler never deletes the output of a removed source file, so the old handler’s compiled code was still sitting in the local build directory. It was never part of the deployed service, since nothing loaded it, but every deploy now rebuilds from an empty directory and refuses to ship compiled code that has no source.

The webapp and the browser remote could be framed by another site

Low · reported and fixed October 2026 · fix d0658fb6 · credit pending the reporter’s preference

The webapp, the browser remote and this site sent nothing telling a browser not to show them inside another site’s frame. A hostile page could load them invisibly and lay its own buttons on top, so a click meant for the hostile page landed on ours (clickjacking).

The reach was limited. A framed copy starts signed out: browsers give a frame inside another site its own storage, so it never sees the keys of the user’s real session, and passkey and Face ID prompts appear above any page, where an overlay cannot reach them. The case that mattered was the browser remote. Someone who scanned its code and approved it with their phone while it sat inside a hostile page would leave an approved session in that frame, where buttons laid over it could steer their clicks.

Every response from flagshipserver.com and its subdomains now refuses framing (frame-ancestors 'none' and X-Frame-Options: DENY), and the webapp also refuses to start inside a frame if it is ever served without those headers. The same pass gave the webapp and the remote a strict Content Security Policy: scripts only from the app itself, no inline script, and connections only to Flagship and the account’s own servers. The pages a Flagship server serves itself (invites, sign-in, request access) refuse framing too, from each server’s next update.

Found something? Report it; the disclosure policy says what to expect.