From 2faa3d808a3a8e7b016e711849845c5ea2ec9217 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sun, 4 Oct 2026 02:21:11 +0200 Subject: [PATCH] Aegis: a WizardConnect request the overlay cannot read is not signed wc-sign accepted a flat request (tx and sourceOutputs at the top) that buildWcApproval does not read, so any paired dapp could get a signature after an overlay showing only the wallet, an input count and the sighash. The signer now takes only the nested WizardConnect shape, and the overlay refuses, without showing anything, a request whose outputs or spent inputs it cannot decode. Total out now sums every output, not the first 8. --- bundled-addons/aegis/index.js | 12 ++++++++++-- bundled-addons/aegis/lib/wc-sign.js | 13 +++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/bundled-addons/aegis/index.js b/bundled-addons/aegis/index.js index e55fa4fe..dca00618 100644 --- a/bundled-addons/aegis/index.js +++ b/bundled-addons/aegis/index.js @@ -980,6 +980,13 @@ function buildWcApproval(payload) { inner = typeof dec === "string" ? null : dec; } catch { inner = null; } } + // Refuse what cannot be shown. The signer only takes this shape (see + // wc-sign.js), and an overlay without outputs or spent inputs is no + // approval at all. + if (!inner || !Array.isArray(inner.inputs) || !Array.isArray(inner.outputs) || !inner.outputs.length + || !Array.isArray(tx.sourceOutputs) || tx.sourceOutputs.length !== inner.inputs.length) { + return { unreadable: true, dappName }; + } const inputCount = Array.isArray(req.inputPaths) ? req.inputPaths.length : (Array.isArray(inner?.inputs) ? inner.inputs.length : "?"); const rows = [{ label: "Wallet", value: walletName }, { label: "Inputs", value: String(inputCount) }]; // What the wallet is putting IN. The outputs alone never showed that a @@ -1011,6 +1018,7 @@ function buildWcApproval(payload) { try { const outs = inner?.outputs || []; let total = 0n; + for (const o of outs) { try { total += BigInt(o.valueSatoshis ?? 0); } catch {} } outs.slice(0, 8).forEach((o, i) => { const lb = o.lockingBytecode; const hexScript = typeof lb === "string" ? lb.toLowerCase() : Buffer.from(lb || []).toString("hex"); @@ -1018,7 +1026,6 @@ function buildWcApproval(payload) { if (/^76a914[0-9a-f]{40}88ac$/.test(hexScript)) addr = ctx.d.cashaddr.encode(prefix, 0, ctx.d.tx.fromHex(hexScript.slice(6, 46))); else if (/^a914[0-9a-f]{40}87$/.test(hexScript)) addr = ctx.d.cashaddr.encode(prefix, 1, ctx.d.tx.fromHex(hexScript.slice(4, 44))); const v = BigInt(o.valueSatoshis ?? 0); - total += v; const token = tokenText(o.token); rows.push({ label: `Output #${i + 1}`, value: `${fmtBch(Number(v))} BCH${token} → ${addr || (hexScript.startsWith("6a") ? "OP_RETURN data" : "script " + hexScript.slice(0, 24) + "…")}`, mono: true }); }); @@ -4150,7 +4157,8 @@ module.exports = { // keys fell back to a lone "OK" button whose id never matched, so // every WizardConnect signing request was refused, and the HTML // body was shown as literal markup. - const { body, rows, dappName } = buildWcApproval(payload); + const { body, rows, dappName, unreadable } = buildWcApproval(payload); + if (unreadable) { api.log(`wc sign: refused an unreadable request from ${dappName}`); return { approved: false }; } const pick = await api.approvalModal({ title: "Sign a Bitcoin Cash transaction (WizardConnect)?", origin: dappName, diff --git a/bundled-addons/aegis/lib/wc-sign.js b/bundled-addons/aegis/lib/wc-sign.js index 34d5ab4c..ffda3774 100644 --- a/bundled-addons/aegis/lib/wc-sign.js +++ b/bundled-addons/aegis/lib/wc-sign.js @@ -66,12 +66,13 @@ async function signTx({ request, account, branches, libauth, secp256k1 }) { // the outputs) meant `tx.inputs` was undefined and signing threw on the // first real request. index.js already read the nested // request.transaction.userPrompt for the approval dialog, so only this - // module had it wrong. The flat shape is still accepted so a caller - // that hands us an already-unwrapped payload keeps working. - const inner = (request.transaction && (request.transaction.transaction !== undefined - || request.transaction.sourceOutputs !== undefined)) - ? request.transaction - : request; + // module had it wrong. Only the nested shape is accepted: the approval + // overlay reads that shape, and a flat request used to be signed after an + // overlay that could show neither its outputs nor what it spends. + const inner = request.transaction; + if (!inner || typeof inner !== "object" || inner.transaction === undefined || !Array.isArray(inner.sourceOutputs)) { + throw new Error("wc-sign: request is not a WizardConnect sign request (request.transaction.{transaction,sourceOutputs})"); + } const tx = ensureTransaction(inner.transaction, libauth); const sourceOutputs = (inner.sourceOutputs || []).map((o, i) => { if (o.contract) throw new Error(`wc-sign: input ${i} spends a contract — unsupported`);