Threat Model & OWASP ASVS L1 Self-Attestation
The threats Vaulted is designed to defeat, the threats it isn't, and a control-by-control attestation against an industry baseline.
Version 1.0 · Published 2026-04-27 · Source: main · Author: Maxim Novak
About this document
This is a self-attestation, not a third-party audit. It exists so you can see exactly what Vaulted claims to protect, what it doesn't, and how each design decision maps to recognised controls. We will commission an independent review and publish its report alongside this page when budget allows. Until then, you can verify the core zero-knowledge claim yourself in five minutes with browser DevTools.
1. Scope & assumptions
This model covers the production deployment of vaulted.fyi including its web frontend, API routes, and Upstash Redis storage. It also covers the published CLI and MCP server insofar as they interact with the same API.
Out of scope:
- The user's endpoint device (browser, OS, extensions, clipboard, screen).
- The communication channel through which the share link is delivered (email, Slack, SMS, etc.).
- The recipient's subsequent handling of the plaintext.
- Vulnerabilities in Vercel or Upstash beneath the API surface we consume.
2. Assets & trust boundaries
Primary asset: the plaintext secret. It must never reach the server, persist outside the sender's and recipient's browsers, or be recoverable from server-side state alone.
Secondary assets: ciphertext, IVs, view counters, expiry timestamps, and the integrity / availability of the service.
Trust boundaries (in order of decreasing trust):
- Sender browser — trusted with plaintext and key.
- Recipient browser — trusted with plaintext and key after entering the link.
- Network between client and server — untrusted; mitigated by TLS 1.3 and the design choice that only ciphertext crosses it.
- Vaulted server (us) — semi-trusted. Treated as a passive ciphertext store. The threat model assumes Vaulted itself could be compromised without exposing plaintext.
- Upstash Redis — same trust level as the server. Encrypted blobs only.
3. Threat actors
Passive network observer. ISP, on-path actor with access to TLS metadata. Defeated by TLS and by the absence of plaintext or key on the wire.
Active network attacker. Can attempt to MitM. Defeated by TLS, HSTS preload, and certificate validation. A successful TLS compromise still yields only ciphertext.
Compromised server / hostile operator. Includes us. Cannot read stored secrets because the key never arrives. Can deny service or modify the served JS bundle — addressed in residual risks below.
Unauthenticated attacker with a valid share link. By design, anyone with the full link can decrypt. View limits, expiry, and optional passphrase reduce the window. This is a deliberate property, not a bug.
Targeted attacker against a specific sender or recipient. Can exploit the endpoint, the delivery channel, or the recipient's post-decryption handling. Outside Vaulted's control; documented for honesty.
4. Controls
- Client-side AES-256-GCM with random 96-bit IVs from
crypto.getRandomValues. - 256-bit keys generated per secret via
crypto.subtle.generateKey; placed in the URL fragment, never transmitted. - Optional passphrase wrapping with PBKDF2 (high iteration count) and AES-GCM key wrap.
- Atomic view-count enforcement via Redis
HINCRBYwith same-transaction deletion at limit. - TTL-based auto-expiry capped at 30 days; no extension after creation.
- Per-IP sliding-window rate limiting on create and view endpoints.
noindexon /s/[id] view pages so secret URLs cannot leak via search.- TLS 1.3 with HSTS preload, secure security headers, no IP logging beyond rate-limit windows.
5. Residual risks (not defended against)
These are deliberate boundary decisions, documented so users can decide whether Vaulted fits their threat model.
- Endpoint compromise. A keylogger, infected browser, or hostile extension can read plaintext after decryption.
- Delivery-channel leak. If the share link is forwarded, archived, or quoted in a replied email thread, that channel becomes the attack surface.
- Targeted bundle swap. A compromised CDN or Vercel edge could serve different JS to a specific user. Subresource Integrity and reproducible builds are partial mitigations and on the roadmap.
- Recipient behaviour. Once decrypted, the recipient can copy, screenshot, or forward.
- Weak passphrase. If used, a weak passphrase is brute-forceable by anyone with the wrapped key.
- Rubber-hose / coercion. Out of scope for any technical control.
6. OWASP ASVS L1 self-attestation
The matrix below maps Vaulted's implementation against selected OWASP ASVS Level 1 requirements. Categories that don't apply (sessions, authentication, file upload) are marked N/A with a brief rationale rather than omitted, so absence of an entry doesn't read as evasion.
| ID | Category | Requirement | Status | Evidence |
|---|---|---|---|---|
| V1.1.1 | Architecture | Secure SDLC with threat modelling for the application | Pass | This document. Reviewed at each material architecture change. |
| V1.4.1 | Architecture | Trusted enforcement points enforce access controls | Pass | API route handlers validate inputs and consume views atomically via Redis HINCRBY in src/lib/redis-secrets-store.ts. |
| V2.10.x | Authentication | Service / user authentication | N/A | Vaulted is anonymous. No user accounts, no service-to-service authentication. Authorisation is by knowledge of the secret ID and URL fragment key. |
| V3.x | Session Management | Session management requirements | N/A | No sessions, no cookies for authenticated state. |
| V5.1.3 | Input Validation | Input validation at trusted service layer | Pass | Manual runtime validation in src/app/api/secrets/route.ts. Payload ≤ 1000 chars enforced server-side; max views and TTL bounded. |
| V5.2.5 | Sanitization | Output encoding for context (HTML, JS, URL) | Pass | React escapes all interpolated values by default. The only dangerouslySetInnerHTML calls render JSON-LD literals constructed server-side from typed data. |
| V6.2.1 | Stored Cryptography | Use approved cryptographic algorithms with safe defaults | Pass | AES-256-GCM via Web Crypto API. NIST SP 800-38D. See src/lib/crypto.ts. |
| V6.2.2 | Stored Cryptography | Cryptographically strong random number generation | Pass | crypto.getRandomValues for IVs and key material. crypto.subtle.generateKey for AES keys. |
| V6.2.5 | Stored Cryptography | No insecure or deprecated cryptographic primitives | Pass | No MD5, SHA-1, DES, RC4, ECB, or CBC-without-MAC anywhere in the cryptographic path. |
| V6.3.1 | Stored Cryptography | Keys protected against unauthorised access | Pass | Encryption key never reaches the server. Lives only in the URL fragment, processed client-side per RFC 3986. Optional passphrase wraps the key with PBKDF2 before sharing. |
| V7.1.1 | Error Handling & Logging | No sensitive information in logs | Pass | Server logs request shape and rate-limit decisions only. No ciphertext, no secret IDs in a way that could be cross-correlated, no IPs persisted beyond the rate-limit window. |
| V7.4.1 | Error Handling & Logging | Generic error messages | Pass | API routes return generic HTTP status codes and short error strings. No stack traces or internal state leaked. |
| V8.1.1 | Data Protection | Sensitive data identified and protected | Pass | Plaintext is never received by the server. Ciphertext is stored encrypted at rest in Upstash Redis with TTL-based deletion and atomic view-limit enforcement. |
| V8.3.1 | Data Protection | Sensitive data not exposed in URLs or referrers | Pass | Encryption key lives in the URL fragment; fragments are never sent to the server and are stripped from document.referrer by browsers per RFC 3986 §3.5. |
| V9.1.1 | Communications | TLS for all client connectivity | Pass | TLS 1.3 enforced at the Vercel edge. HSTS preloaded. HTTP requests redirect to HTTPS. |
| V10.3.2 | Malicious Code | Application code reviewed for malicious code | Pass | Single-maintainer codebase. All commits authored by the project owner; signed commits where supported. Dependency updates via Dependabot reviewed before merge. |
| V11.1.1 | Business Logic | Logic flows protected against abuse | Pass | Per-IP rate limiting via Upstash Ratelimit (10 creates/min, 30 views/min). View counts decremented atomically. |
| V12.x | File & Resources | File upload and processing requirements | N/A | Vaulted does not accept file uploads. |
| V13.2.1 | API & Web Services | RESTful endpoints validate request schema | Pass | All POST/GET handlers validate path params, body shape, and content-type before processing. |
| V14.4.3 | Configuration | Standard security headers configured | Pass | HSTS, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy set via Next.js config. Content-Security-Policy enforced per-request in src/proxy.ts with a nonce-based strict-dynamic script-src; API routes get a locked-down default-src none policy. |
Numbering follows the ASVS 4.0 structure. The list is selected, not exhaustive — the goal is honesty about the controls that meaningfully apply to a stateless, account-less encrypted ciphertext store.
7. Change process
Any change that affects an item in this document — new endpoint, cryptographic change, dependency-affecting refactor, infrastructure change — must trigger a review of this page in the same PR. The version and published date above will increment when the document is updated; the prior version remains accessible in the public git history.
Sign-off
This threat model is published in good faith by the maintainer of Vaulted. Errors, omissions, and disagreements with the attestations above are explicitly invited — please report them to [email protected] or via the responsible-disclosure programme.
Maxim Novak — maintainer, vaulted.fyi · 2026-04-27