feat(aegis): the BCH bridge works on any site, not just .x

window.bitcoincash was gated on a ".x" hostname in both the preload and the
host handlers — inherited from the pre-multi-wallet build, when every dapp
Aegis knew about was a Silent Mode one. It was never a security boundary: the
TRX / ETH / SOL bridges have always injected everywhere, and every BCH call
still crosses into main, raises an approval overlay and honours per-origin
permissions. The preload cannot read an address or sign anything by itself.

What the gate actually cost was Cauldron, bch.guru and our own dapps on other
TLDs (potidaea.asm) seeing no BCH provider at all, on a wallet whose entire
subject is BCH. WizardConnect was never gated this way, so the two halves of
the same bridge disagreed about who could talk to it.

isBchOrigin becomes isDappOrigin and now only keeps non-web schemes out —
file://, chrome://, data:, javascript: and anything unparseable.

Separately: page-inject declared https://*/* only, so a dapp served from
http://127.0.0.1 got no preload at all, which is a different cause from the
.x gate and would have survived removing it. localhost, 127.0.0.1 and [::1]
are now matched over http as well (the host's compiler makes the port
optional, so :3000 and :5173 are covered). Plain http on any other host stays
out, since an http page is tamperable in transit and only local development
needs the exception.
This commit is contained in:
Local Dev 2026-10-03 09:45:01 +02:00
parent 40e7fe2ae9
commit 64b75c5bf4
3 changed files with 38 additions and 22 deletions

View file

@ -1,9 +1,9 @@
{ {
"id": "aegis", "id": "aegis",
"name": "Aegis Wallet", "name": "Aegis Wallet",
"version": "0.26.1", "version": "0.27.0",
"category": "plugin", "category": "plugin",
"description": "Multi-chain wallet (BCH, BTC, TRX, ETH, SOL, SC, DGB) derived from your Theseus vault. Dapps get window.bitcoincash on .x sites; window.tronWeb / window.tronLink / window.ethereum / window.solana on any https page.", "description": "Multi-chain wallet (BCH, BTC, TRX, ETH, SOL, SC, DGB) derived from your Theseus vault. Dapps get window.bitcoincash and window.wizardconnect on any site; window.tronWeb / window.tronLink / window.ethereum / window.solana too. Every call needs your approval.",
"author": "Silent Mode", "author": "Silent Mode",
"icon": "data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='none'%3E%3Cpolygon points='16,2 28,9 28,23 16,30 4,23 4,9' fill='%230a0a0d' stroke='%23D6FF3D' stroke-width='1.6' stroke-linejoin='round'/%3E%3Ccircle cx='16' cy='16' r='4.5' fill='none' stroke='%23D6FF3D' stroke-width='1.4'/%3E%3Ccircle cx='16' cy='16' r='1.6' fill='%23D6FF3D'/%3E%3C/svg%3E", "icon": "data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='none'%3E%3Cpolygon points='16,2 28,9 28,23 16,30 4,23 4,9' fill='%230a0a0d' stroke='%23D6FF3D' stroke-width='1.6' stroke-linejoin='round'/%3E%3Ccircle cx='16' cy='16' r='4.5' fill='none' stroke='%23D6FF3D' stroke-width='1.4'/%3E%3Ccircle cx='16' cy='16' r='1.6' fill='%23D6FF3D'/%3E%3C/svg%3E",
"main": "index.js", "main": "index.js",
@ -23,7 +23,10 @@
"page-inject": { "page-inject": {
"preload": "wallet-inject.js", "preload": "wallet-inject.js",
"origins": [ "origins": [
"https://*/*" "https://*/*",
"http://localhost/*",
"http://127.0.0.1/*",
"http://[::1]/*"
] ]
} }
} }

View file

@ -2492,10 +2492,15 @@ async function withOriginLock(origin, fn) {
// Only .x sites (BCNR-native TLD) get the BCH bridge, matching the pre- // Only .x sites (BCNR-native TLD) get the BCH bridge, matching the pre-
// multi-wallet gate. Widening the manifest to https://*/* makes the Tron // multi-wallet gate. Widening the manifest to https://*/* makes the Tron
// bridge available everywhere; the BCH side enforces its narrower rule // bridge available everywhere.
// inside the handlers. // Any ordinary web origin may talk to the BCH bridge. The old test here
function isBchOrigin(origin) { // required a ".x" hostname, which locked out every third-party BCH dapp
try { const h = new URL(origin).hostname; return /\.x$/.test(h); } // (Cauldron, bch.guru) and our own dapps on other TLDs — while the TRX / ETH
// / SOL bridges and WizardConnect accepted any origin all along. The real
// protection is downstream and unchanged: an approval overlay on every
// action plus per-origin permissions. This only keeps non-web schemes out.
function isDappOrigin(origin) {
try { const u = new URL(origin); return u.protocol === "https:" || u.protocol === "http:"; }
catch { return false; } catch { return false; }
} }
function legacyBchRuntime() { function legacyBchRuntime() {
@ -2518,7 +2523,7 @@ function registerPageMessages(api) {
// ---- BCH bridge (unchanged behavior; wallet source is legacy default) ---- // ---- BCH bridge (unchanged behavior; wallet source is legacy default) ----
api.onMessage("getAddress", async (_p, m) => { api.onMessage("getAddress", async (_p, m) => {
const origin = fromPage(m); const origin = fromPage(m);
if (!isBchOrigin(origin)) throw new Error("this site is not on a Bitcoin Cash origin"); if (!isDappOrigin(origin)) throw new Error("this page cannot use the wallet bridge");
const rt = legacyBchRuntime(); const rt = legacyBchRuntime();
const perms = permissions(api); const perms = permissions(api);
if (perms[origin] && perms[origin].readAddress) return rt.adapter.current().address; if (perms[origin] && perms[origin].readAddress) return rt.adapter.current().address;
@ -2538,7 +2543,7 @@ function registerPageMessages(api) {
}); });
api.onMessage("signAndSend", async (p, m) => { api.onMessage("signAndSend", async (p, m) => {
const origin = fromPage(m); const origin = fromPage(m);
if (!isBchOrigin(origin)) throw new Error("this site is not on a Bitcoin Cash origin"); if (!isDappOrigin(origin)) throw new Error("this page cannot use the wallet bridge");
const rt = legacyBchRuntime(); const rt = legacyBchRuntime();
return withOriginLock(origin, async () => { return withOriginLock(origin, async () => {
let plan; let plan;
@ -2592,7 +2597,7 @@ function registerPageMessages(api) {
}); });
api.onMessage("signMessage", async (p, m) => { api.onMessage("signMessage", async (p, m) => {
const origin = fromPage(m); const origin = fromPage(m);
if (!isBchOrigin(origin)) throw new Error("this site is not on a Bitcoin Cash origin"); if (!isDappOrigin(origin)) throw new Error("this page cannot use the wallet bridge");
const rt = legacyBchRuntime(); const rt = legacyBchRuntime();
const message = String(p && p.message != null ? p.message : ""); const message = String(p && p.message != null ? p.message : "");
if (message.length > 4096) throw new Error("message too long"); if (message.length > 4096) throw new Error("message too long");

View file

@ -19,18 +19,26 @@ const call = (msg, payload) =>
throw new Error(text); throw new Error(text);
}); });
// -------- BCH bridge (window.bitcoincash), only on .x pages ------------------ // -------- BCH bridge (window.bitcoincash), everywhere -----------------------
const isBchOrigin = (() => { try { return /\.x$/i.test(location.hostname); } catch { return false; } })(); //
if (isBchOrigin) { // This used to be gated on a `.x` hostname, inherited from the
theseus.contextBridge.exposeInMainWorld("bitcoincash", { // pre-multi-wallet build where every dapp Aegis knew about was a Silent Mode
// one. It was never a security boundary: the TRX / ETH / SOL bridges have
// always injected on every page, and every call here still crosses into
// main, raises an approval overlay and honours per-origin permissions.
// Nothing in this file can read an address or sign anything on its own.
//
// Keeping it meant Cauldron, bch.guru and our own non-.x dapps (potidaea.asm)
// saw no BCH provider at all, on a wallet whose whole point is BCH.
// WizardConnect below was never gated this way, so the two halves disagreed.
theseus.contextBridge.exposeInMainWorld("bitcoincash", {
isTheseus: true, isTheseus: true,
version: "0.2.0", version: "0.2.0",
network: "mainnet", network: "mainnet",
getAddress: () => call("getAddress"), getAddress: () => call("getAddress"),
signAndSend: (txSpec) => call("signAndSend", txSpec && typeof txSpec === "object" ? txSpec : {}), signAndSend: (txSpec) => call("signAndSend", txSpec && typeof txSpec === "object" ? txSpec : {}),
signMessage: (message) => call("signMessage", { message: String(message ?? "") }), signMessage: (message) => call("signMessage", { message: String(message ?? "") }),
}); });
}
// -------- Tron bridge (window.tronWeb, window.tronLink), everywhere --------- // -------- Tron bridge (window.tronWeb, window.tronLink), everywhere ---------
// //