Internal Security Self-Audit
Published unedited on 10 July 2026. This is the grey-box review VaultPass ran against its own source in June 2026, modelled on Cure53's methodology. It found no Critical and no High severity issues. The Low and Informational findings below remain open unless marked otherwise.
It is reproduced exactly as written — including the instruction near the top telling ourselves to keep it off the public site. That line is the reason this page exists. An internal audit that names its own unfixed weaknesses is worth more to you than a clean report nobody can inspect.
This is not an independent audit. It was performed by the person who wrote the code. VaultPass has no external audit; see the audit roadmap for what that will take and when.
EDITOR'S NOTE — 10 JULY 2026
Section 3 of the audit below states that “the server never receives plaintext or the full key.” That is literally true and materially incomplete, and we are not editing the original to hide it.
The vault key is split 2-of-3 with Shamir's Secret Sharing. VaultPass servers hold two of those three shards — the heir shard and the sentinel shard. Both are encrypted at rest under a key derived from a server-side secret, so a database breach alone recovers nothing. But an operator holding that secret alongside the database holds a reconstructing quorum. VaultPass can therefore, in principle, derive your vault key.
This is why claims like “mathematically impossible for us to read your vault” were removed from this site in June 2026. Removing the server from the quorum is the open roadmap item that would earn the claim back.
# VaultPass — Internal Security Audit (Cure53-style)
**Date:** 2026-06-14
**Type:** Grey-box source review (self-assessment, prep for external Cure53 engagement)
**Scope:** Next.js web app — crypto core, auth, all API routes, succession/heir flow, headers/CSP
**Method:** Manual source review + dependency/secret scan. No live exploitation.
> This is an *internal* pre-audit modelled on Cure53's methodology so the external
> engagement starts from a clean baseline. Keep it out of the public site. Finding
> IDs follow Cure53 convention `VP-26-NNN`.
---
## 1. How Cure53 actually runs an audit (so we can mirror it)
Cure53 is a Berlin pen-test firm specialising in web/crypto/browser security. A typical engagement:
1. **Kickoff & scoping** — you give them: source repo access (grey-box), a staging
instance with seeded test accounts, a threat model, and the list of "crown-jewel"
assets (here: vault seed phrases, Shamir shards, heir-release logic).
2. **Coverage matrix** — they enumerate every route/component and tick off what was
reviewed, so gaps are explicit. Effort is sold in *person-days* (small SaaS ≈ 6–12 pd).
3. **Manual review first, tools second** — senior testers read the code and probe the
running app by hand; automated scanners (Burp, semgrep, dependency audit) are
supplementary, not the core.
4. **Findings get IDs + CVSS-ish severity** — `Critical / High / Medium / Low / Info`.
Each finding: location, description, reproduction/PoC, impact, fix recommendation.
5. **"Miscellaneous Issues"** — hardening items that aren't directly exploitable.
6. **Report + fix-verification round** — they re-test after you patch; the final report
notes which findings are resolved. Many teams publish the report once clean.
What they hammer for a product like this: **crypto correctness** (IV reuse, key
derivation, integrity), **authorization/IDOR**, **secret handling**, **the
business-logic of the Dead Man's Switch & heir release** (can a heir trigger early? can
an attacker forge a switch event?), **CSP/headers**, and **dependency CVEs**.
---
## 2. Scope & coverage
| Area | Files | Reviewed |
|------|-------|----------|
| Client crypto (AES-GCM, SSS) | `lib/crypto.ts`, `lib/workers/crypto.worker.ts`, `lib/shamir.ts` | ✅ |
| Server shard crypto | `lib/shard-crypto.ts` | ✅ |
| Auth / MFA / reset | `app/auth/*`, `app/api/account`, mfa-recovery | ✅ |
| Vault API | `/api/vault`, `/api/import` | ✅ |
| Succession/heir | `/api/heir*`, `/api/check-in*`, `/api/trusted-circle/*` | ✅ |
| Payments/webhooks | `/api/stripe/*`, `/api/coinbase/*`, `/api/resend/webhook`, `/api/inngest` | ✅ |
| Headers / CSP | `next.config.mjs` + Cloudflare layer | ✅ |
| Secrets in repo | full tree | ✅ |
---
## 3. Identified vulnerabilities
**No Critical or High findings.** The zero-knowledge model holds: seed phrases are
AES-256-GCM encrypted client-side, the key is split via Shamir before anything is
stored, and the server never receives plaintext or the full key.
The following were found earlier in this cycle and are **already fixed** (listed for the
external team's fix-verification round):
- **VP-26-001 (Medium, RESOLVED 2026-06-14)** — *Account deletion lacked server-side
step-up auth.* `DELETE /api/account` wiped all data on the session cookie alone;
password check was client-side only → a stolen/forced cookie could delete an account
directly. **Fix:** server now re-verifies the password via anon `signInWithPassword`
before any delete (commit `3876540`).
- **VP-26-002 (Medium, RESOLVED)** — *Raw DB error leakage.* ~12 routes returned raw
Supabase/Postgres `error.message` to clients (schema/PII leak). **Fix:** generic
client messages + server-side `console.error` (commit `5aae1db`).
- **VP-26-003 (Medium, RESOLVED)** — *Rate limiting fail-open in prod.* Upstash env
vars were absent in production, so every `Ratelimit` silently allowed all traffic
(brute-force on MFA-recovery / reset). **Fix:** Upstash provisioned + wired; limiters
fail-closed where destructive (commits `5aae1db`/`4388bcf`).
---
## 4. Miscellaneous issues (hardening — Low / Info)
- **VP-26-101 (Low, RESOLVED 2026-06-14) — Production CSP was overridden by Cloudflare.**
The live CSP carried `'unsafe-eval'` + a dead `https://plausible.io`. Root cause: a
Cloudflare **Worker `security-header`** bound to `www.vaultpass.network/*` rewriting
responses with a stale CSP (not a Transform Rule — those were empty). **Fix:** removed
the Worker route so Vercel's hardened header (`next.config.mjs`) passes through.
Verified live: `script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://js.stripe.com`,
no plausible, HSTS/X-Frame-Options/etc intact. CSP is now single-source in code.
- **VP-26-102 (Info) — CSP relies on `'unsafe-inline'`** for script/style (Next.js App
Router hydration). Move to nonce-based CSP once Next nonce support is stable. Known
constraint; flag as accepted in the engagement scope.
- **VP-26-103 (Low) — Shamir shares are not individually authenticated.** A corrupted
share is only detected downstream when AES-GCM decryption fails; the system cannot
identify *which* share was bad, so a single malicious heir can deny reconstruction
(availability, not confidentiality). Consider a per-share MAC/checksum. Confidentiality
is intact (GCM tag is end-to-end).
- **VP-26-104 (Info) — HMAC-KDF message not canonicalised.** `shard-crypto.ts`
derives keys from `HMAC(secret, "${userId}:${nodeId}")`. Colon-delimited concatenation
is ambiguous in theory; not exploitable because `userId` is a Supabase UUID. Use a
length-prefixed/structured input for robustness.
- **VP-26-105 (Info) — Missing input validation in crypto deserialization.**
`reconstructKey` / `_deser` / `decryptShard` `split()` then index without checking part
count or numeric parsing (`parseInt` → `NaN`). Malformed input throws unhandled rather
than a clean error. Add format guards.
- **VP-26-106 (Info) — `String.fromCharCode(...arr)` spread** in `lib/crypto.ts` will
hit call-stack limits on large buffers. Safe for seed-phrase sizes; risky if reused for
bulk vault data. Prefer a chunked/`Buffer`-based base64.
- **VP-26-107 (Low, ACCEPTED) — heir-confirm token-state oracle.** `GET /api/heir-confirm`
returns distinct `404` (no token) vs `410` (expired) vs redirect (already confirmed),
leaking token existence. Infeasible to exploit (tokens are SHA-256 of 122-bit UUIDs);
the distinct messages are genuine heir UX. Documented as accepted risk.
- **VP-26-108 (Info) — Shamir logic is triplicated.** GF(256) SSS is implemented inline
in `lib/crypto.ts` AND mirrored in `lib/workers/crypto.worker.ts`, while
`lib/shamir.ts` is a separate, apparently-unused implementation. Triplication = drift
risk + larger audit surface. Consolidate to one reviewed module; delete the unused one.
- **VP-26-109 (Info) — Account deletion is not atomic.** `/api/account` issues parallel
table deletes then `admin.deleteUser` with no transaction; a partial failure leaves
orphaned rows (data-hygiene / residual-data concern for a privacy product). Wrap in a
Postgres function/RPC for all-or-nothing deletion.
---
## 5. Crypto verdict (the part Cure53 cares about most)
- AES-256-GCM, fresh 256-bit key per seed, fresh random 96-bit IV per encryption, GCM
auth tag retained → **no IV reuse, integrity protected.** ✅
- Server shard encryption: per-(user,nodeType) key via HMAC-SHA256 over a high-entropy
env secret, random IV, stored tag. ✅
- Shamir GF(256): correct multiplicative inverse (`x^254`), consistent reduction poly,
2-of-3 threshold, coefficients from CSPRNG → information-theoretic secrecy holds. ✅
Residual crypto items are all Low/Info (above). **The core is sound enough to hand to an
external auditor with confidence.**
---
## 6. Pre-engagement checklist for Cure53
- [x] **Disclosure contact consolidated to [email protected]** (2026-06-14) —
security@ is no longer published anywhere; security.txt + all security pages now
point reporters/auditors to support@, the already-live mailbox. One inbox to
monitor (see §7).
- [ ] Staging URL + 2–3 seeded test accounts (owner + heir + sentinel roles).
- [ ] Read access to this repo (grey-box).
- [ ] Hand them this doc + the threat model (zero-knowledge claim, DMS, heir release).
- [ ] Fix VP-26-101 (Cloudflare CSP) before they start so it's not noise.
---
## 7. Disclosure contact — resolved (consolidated to support@)
Originally the repo advertised **two** addresses: `support@` (general contact + live
`From:` sender on all transactional email) and `security@` (disclosure contact, in
`security.txt` + the /security pages). Maintaining a separate `security@` mailbox that
must never bounce was an avoidable ops burden.
**Decision (2026-06-14):** consolidate. All public disclosure references were changed
`security@` → `support@` (security.txt, /security, /security-policy, /security/bug-bounty,
/hall-of-fame, /docs, /mission, messages/*). `support@` is already the monitored, live
mailbox (it's the From: sender), so disclosure mail now lands in an inbox that exists.
Why this matters: `security.txt` is the RFC 9116 file scanners/researchers read first, and
Cure53's intake will email the published address. Advertising an address that bounces is
worse than none — now there's a single, working contact.
Found something we missed? [email protected] — corrections are published, not buried.