In May 2026 a periodic adversarial security pass audited the running
product against its own source code. The prior full pass was the
security review summarized on
/security; this one verified
findings against actual source files with file-and-line citations,
not against the project's own documentation, because docs can be
aspirational and the code is what users run. It ran on 2026-05-18 as
five parallel deep-dives across cryptography, web security, public
copy, pricing, and legal and ops.
The pass surfaced sixteen must-fix items, and over the following weeks they were closed. Of the sixteen, only a handful were genuine zero-knowledge or security-boundary issues. The rest were truthful-copy corrections, legal and compliance paperwork, and UX or ops gaps.
The highest-severity finding: a server-side plaintext handoff
The top item was a server handoff that should never have existed.
The auth flow POSTed the plaintext vault password to
/api/vault-handoff/set, which wrote it into a
__Host-shieldfive_vault_handoff cookie. The Next.js handler
received the plaintext password in its request body, which
contradicts the "server never sees plaintext or keys" claim the
whole product rests on. It existed because an earlier convenience
fix reached for a server round-trip instead of staying client-side,
to keep the vault unlocked across a hard reload from /auth to
/files.
The fix (commit c95012e3) deleted the entire server-side handoff:
the app/api/vault-handoff/ routes and utils/vaultUnlockHandoff.ts.
Surviving a same-tab hard reload now relies on the keyring's existing
wrapped-root-key session record, with the unlock modal as the
fallback. The old routes remain as 410 stubs that clear the legacy
cookie for any stale bundle still calling them. The leak was
self-inflicted by a feature, and auditing against the code rather
than the docs is what caught it.
Auth and abuse hardening
Several findings were account-takeover and enumeration gaps.
MFA-unenroll, delete-account, and change-email now require AAL2 plus
a fresh credential, through a shared utils/mfa.ts helper. That
helper fails closed: if the assurance-level lookup errors, it returns
requiresStepUp: true rather than silently passing, surfacing a
distinct 503 so the gate stays shut. Sign-in, forgot-password, and
reset now collapse distinct backend errors to one user-visible
string, with the real cause logged server-side, removing the message
tells that enumerate accounts. A signup hardening pass added
behavior-score gating with adaptive Turnstile, per-IP rate-limiting,
and a reaper for never-verified accounts. On billing, Stripe webhook
processing got an atomic duplicate-delivery claim and append-only
writes gated through SECURITY DEFINER RPCs.
Making the claims match the code
Most of the sixteen were not crypto. The audit found fabricated
trust signals shipped in the locale bundle: invented testimonials, an
unsourced "30+ countries" stat, and a "500 spots remaining" scarcity
block. These were removed, and the trust-stat copy was replaced with
four sourced facts (the internal-review finding count, its severity
breakdown, the build-log post count, and the Apache-2.0 crypto
license). A stale comparison claim about a competitor's post-quantum
support was rewritten to distinguish the products accurately. A
non-existent "ShieldFive Labs" legal entity was replaced everywhere
with the real operator. The FAQ still described a removed
per-file-password product and was rewritten to the current
single-vault-key model. The privacy policy was spun out as a
standalone route that discloses third-party recipients by category
under GDPR Art. 13(1)(e), with the named sub-processor list available
on request rather than published; refund copy was reconciled with the
Terms, and an AUP plus an RFC 9116 security.txt landed. These are
credibility and compliance gaps, not boundary breaks.
Crypto-library work since PQ-default
In parallel, @shieldfive/crypto moved from alpha.7 to alpha.11. It
added Suite 0x04 (aes-gcm-v2), which widens the per-file nonce
prefix from 4 to 8 bytes and shrinks the cross-file IV-collision
probability, plus optional detached Ed25519 sender-attribution
signatures. The @noble/post-quantum dependency was bumped to
^0.6.0. The default suite is unchanged: new files still use Suite
0x03, the ML-KEM-1024 plus XChaCha20-Poly1305 hybrid. Suite 0x04
is opt-in via a dedicated subpath import only.
A separate follow-up, not one of the sixteen, fixed an int4-overflow in the daily egress cap that had been 503-ing authenticated downloads. It landed out-of-band on 2026-06-04, after the audit.
What this still doesn't fix
An audit is a point-in-time pass. It verifies the code as it stands on the day it runs, and a later change can always reintroduce what it cleared. The only real guard against that is running it again.
Links
- Posture:
/security. - Prior post: Phase 3 — post-quantum hybrid is the default.
- Source: github.com/shieldfive.