Skip to main content
← SECURITY

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.