Browser security has two halves: cryptography (WebCrypto gives you real AES, HKDF, and ECDH with no libraries) and isolation (sandboxes that contain untrusted code). This guide covers both — with the honest threat models, because crypto without a threat model is just math.
No libraries, no hand-rolled ciphers: crypto.subtle gives you audited primitives. The workhorse is AES-GCM (authenticated encryption — it detects tampering):
Three rules that matter more than the code: never reuse an IV with the same key, never skip the KDF step (raw passwords aren't keys), and store the salt alongside the ciphertext — it's not secret.
Deriving your encryption key and your HMAC key from the same master secret is a classic mistake. HKDF derives independent, purpose-bound sub-keys from one master:
The info parameter is the whole point: different info strings → cryptographically independent keys. Compromise of one purpose never leaks another.
The honest threat model: client-side encryption protects data at rest from other origins, stolen backups, and curious server admins. It does not protect against a compromised device, malicious browser extensions, or XSS in your own page — if an attacker runs JS in your origin, they can read your keys. "Zero-knowledge" in a web app means the server never sees plaintext, not that the client is invulnerable.
Running user plugins or third-party widgets? The sandboxed iframe is the battle-tested primitive — it strips capabilities by default and you add back only what's needed:
Without allow-same-origin, the iframe gets an opaque origin — it can't touch your DOM, cookies, or storage even if its script is malicious. Communicate across the boundary with postMessage and always validate event.origin. This is the pattern every serious plugin system (and every online code playground's preview pane) is built on.
Fenced frames (<fencedframe>) go further than sandboxed iframes: the embedder can't even observe what's inside — no reading its size, URL, or events. The honest context: this was built for privacy-preserving advertising (so ad tech can't fingerprint users across sites), it's Chromium-only, and for most app plugin systems a sandboxed iframe is the right, portable choice.
Sometimes an iframe is too heavy — you want to run untrusted JavaScript (a user formula, a plugin script) in the same page. ShadowRealm, a Stage 3 TC39 proposal, creates a fresh JS realm with its own globals and no DOM access:
Until it standardizes, the practical equivalents are sandboxed iframes, workers with no sensitive data, or vetted sandboxing libraries. Don't build production security on a proposal.
Insertable streams let you encrypt WebRTC audio/video frames before they hit the network — true end-to-end encryption for calls:
The critical honesty: E2EE protects media in transit, but WebRTC still needs signaling (exchanging SDP offers/answers requires a server or manual copy-paste) and key exchange (the shared key must reach both peers securely — ECDH via WebCrypto is the standard answer). "P2P" never means "no server at all."
Security checklist for any browser app: ① HTTPS everywhere (WebCrypto won't even expose some APIs on http), ② Content Security Policy headers, ③ sandboxed iframes for untrusted content, ④ KDF before encryption, fresh IVs, ⑤ validate every postMessage origin. Get these five right and you're ahead of most of the web.
WebCrypto, sandboxing, and isolation — experiment in a local-first browser IDE.
Launch NitroIDE