From b292408ceadb9848f5f49e4e2f189246f1d18aca Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sun, 4 Oct 2026 04:11:42 +0200 Subject: [PATCH] Theseus: credit a wiz:// pairing link only to the top document will-navigate and the window-open handler routed every wiz:// link to the wallet with the top-level URL's origin, whichever frame raised it. An ad iframe on a trusted dapp could window.open() a pairing URI and the pairing prompt would name the trusted site; one click on Pair gave the attacker the wallet's xpubs and a standing signing channel. A link must now come from the main frame (navigation initiator, or for window.open the referrer's origin), and only on pages where the wallet is allowed by the inject policy. --- main.js | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/main.js b/main.js index 05c6ee04..a06a4d52 100644 --- a/main.js +++ b/main.js @@ -4015,7 +4015,15 @@ function createTab(initial, opts = {}) { // wallet and leaves the dapp exactly where it is. if (parsed.protocol === "wiz:") { e.preventDefault(); - routeWizUri(u, pageOriginOf(wc.getURL()), tab.id); + // Only the top document speaks for the top origin. A cross-origin + // iframe (an ad on a trusted dapp) could navigate _top to a wiz:// + // link and the pairing prompt would name the trusted site. + const init = e.initiator; + if (init && init.frameTreeNodeId !== wc.mainFrame.frameTreeNodeId) { + console.log("[wiz] ignored a pairing link from a sub-frame"); + return; + } + routeWizUri(u, wc.getURL(), tab.id); return; } if (parsed.protocol === "bns:") return; @@ -4063,11 +4071,24 @@ function createTab(initial, opts = {}) { if (choice === 1) e.preventDefault(); // Leave anyway -> override the beforeunload }); // Links that open a new tab: target="_blank", window.open, Ctrl/middle-click. - wc.setWindowOpenHandler(({ url, disposition }) => { + wc.setWindowOpenHandler((details) => { + const { url, disposition } = details; // target="_blank" on a wiz:// link lands here rather than will-navigate. // Route it to the wallet instead of opening a tab on an unloadable URL. if (url && /^wiz:/i.test(url)) { - routeWizUri(url, pageOriginOf(wc.getURL()), tab.id); + // The open handler does not say which frame called window.open, but + // the referrer does: a link or window.open from the top document + // carries the top origin. Anything else (a sub-frame, or a referrer + // stripped with noreferrer) is not credited to the top site. + let refOrigin = null; + try { refOrigin = details.referrer && details.referrer.url ? new URL(details.referrer.url).origin : null; } catch {} + let topOrigin = null; + try { topOrigin = new URL(wc.getURL()).origin; } catch {} + if (!refOrigin || refOrigin !== topOrigin) { + console.log("[wiz] ignored a pairing window from another frame or without a referrer"); + return { action: "deny" }; + } + routeWizUri(url, wc.getURL(), tab.id); return { action: "deny" }; } const installId = installLinkId(url); @@ -5703,10 +5724,18 @@ function pageOriginOf(url) { // The wallet still shows its own approval before anything is paired; all // this does is carry the URI across, tagged with the origin that offered it // so the approval can name the real site. -function routeWizUri(uri, origin, tabId) { +// `pageUrl` is the top document's URL; the pairing is credited to its origin +// only if the wallet may be on that page at all (the same inject policy that +// decides whether the page gets the wallet bridge). +function routeWizUri(uri, pageUrl, tabId) { if (!addonHost) return false; const u = String(uri || ""); if (!/^wiz:/i.test(u) || u.length > 4096) return false; + const origin = pageOriginOf(pageUrl); + if (!origin || !addonHost.pageAllowed("aegis", String(pageUrl || ""))) { + console.log("[wiz] pairing link ignored: the wallet is not enabled on this page"); + return false; + } const send = () => addonHost .dispatch("aegis", "wcConnectFromPage", { uri: u }, { from: "page", origin: origin || "unknown site", tabId }) .catch((err) => console.log("[wiz] pairing failed:", err?.message || err));