Is Revoke.cash - web3 scam protection safe?
Revoke.cash - web3 scam protection is high risk. Starting a wallet transaction or signature request, the content script forwards details to the SW, logged as "Message received". Details can include addresses, calldata, signature data, chain ID, hostname, a user ID. Evidence is code-based.
Who publishes itKalis Software B.V. - no other listings under this identity, 4 shared hostnames
Kalis Software B.V. - no other listings under this identity, 4 shared hostnames
What this publisher told the store about itself, and the other listings that told it the same thing.
Shared hosts - 4 hostnames
Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Wallet messages are forwarded to Amplitude analytics
Starting a wallet transaction or signature request, the content script forwards details to the SW, logged as "Message received".
Details can include addresses, calldata, signature data, chain ID, hostname, a user ID.
Evidence is code-based.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You start a protected wallet action on a website.
The covered requests include transactions, typed signatures, personal-sign messages, wallet_sendCalls, and Coinbase wallet signing requests.
The extension forwards the wallet request details to its service worker and labels the full object as analytics.
The service worker calls the tracking helper with a Message received event whose message property is the original wallet-message object.
- Website hostnameapp.uniswap.org (illustrative)
Shows which site you were using when the wallet request was processed.
- Wallet request typeeth_sendTransaction (illustrative)
Shows what kind of wallet action you were asked to approve or sign.
- Wallet address fieldsfrom=0x742d35Cc6634C0532925a3b844Bc454e4438f44e, to=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 (illustrative)
Can identify which wallet address was involved in the request.
- Transaction calldata0x095ea7b30000000000000000000000001111111254eeb25477b68fb85ed929f73a9605820000000000000000000000000000000000000000000000000000000000000000 (illustrative)
Can reveal the contract method and parameters behind the action you were asked to approve.
- Signature message contentpersonal_sign message: Sign in with Ethereum to app.uniswap.org (illustrative)
Can include the message or typed data you were asked to sign.
- Persistent extension user IDf3a742c1-8e10-44d2-bc6a-9d5e0c2a7814 (illustrative)
Lets analytics associate separate wallet-message events with the same extension installation over time.
Dynamic analysis captured Amplitude POST traffic during onboarding, but those captured bodies did not contain the wallet-message indicators.
The content script packages wallet requests with hostname and chain ID
Readable content-script excerpt
js/content-scripts/bypass-check.jsconst os = (e, t) => { const n = as(t); e.postMessage({ requestId: n, data: t })};let is = 0;const as = e => { const t = ++is; return e.type === ns.TRANSACTION ? ss()({ ...e.transaction, _nonce: t }) : e.type === ns.BATCH_TRANSACTION ? ss()({ _batch: e.transactions, _nonce: t }) : e.type === ns.TYPED_SIGNATURE ? ss()({ ...e.typedData, _nonce: t }) : e.type === ns.UNTYPED_SIGNATURE ? ss()({ _msg: e.message, _nonce: t }) : ss()({ ...e, _nonce: t })};let cs = 1;const us = !0;window.addEventListener("message", e => { const { target: t } = e?.data ?? {}, { name: n, data: r } = e?.data?.data ?? {}, { hostname: s } = location, o = cs; if ("metamask-provider" !== n || !r) return; const i = e => { Array.isArray(e) ? e.forEach(e => { if (e) if (ls(e, "eth_sendTransaction")) { const [t] = e.params ?? [], n = ns.TRANSACTION, r = es().runtime.connect({ name: ts }); os(r, { type: n, bypassed: us, hostname: s, transaction: t, chainId: o }) } else if (ls(e, "eth_signTypedData")) { const [t, n] = e.params ?? [], r = JSON.parse(n), i = ns.TYPED_SIGNATURE, a = es().runtime.connect({ name: ts }); os(a, { type: i, bypassed: us, hostname: s, address: t, typedData: r, chainId: o }) } else if (ls(e, "eth_sign") || ls(e, "personal_sign")) { const [t, n] = e.params ?? [], r = 40 === String(t).replace(/0x/, "").length ? n : t, o = ns.UNTYPED_SIGNATURE, i = es().runtime.connect({ name: ts }); os(i, { type: o, bypassed: us, message: r, hostname: s }) } else if (ls(e, "wallet_sendCalls")) { const [t] = e.params ?? [], { from: n = "0x0000000000000000000000000000000000000000", calls: r } = t ?? {}, i = ns.TRANSACTION, a = es().runtime.connect({ name: ts }); for (const e of r) { const t = { from: n, ...e }; os(a, { type: i, bypassed: us, hostname: s, transaction: t, chainId: o }) } } }) : i([e]) }; "metamask-contentscript" === t && i(r), "metamask-inpage" === t && r?.method?.includes("chainChanged") && (cs = Number(r?.params?.chainId ?? cs))}), window.addEventListener("message", e => { const { type: t, data: n } = e?.data ?? {}, { hostname: r } = location; if ("extensionUIRequest" === t && n) { if ("signEthereumTransaction" === n.request?.method) { const e = { from: n.request.params.fromAddress, to: n.request.params.toAddress, data: n.request.params.data, value: Number.parseInt(n.request.params.weiValue ?? "0").toString(16) }, t = Number(n.request.params.chainId ?? 1), s = ns.TRANSACTION, o = es().runtime.connect({ name: ts }); os(o, { type: s, bypassed: us, hostname: r, transaction: e, chainId: t }) } if ("signEthereumMessage" === n.request?.method) { const e = n.request.params.typedDataJson, t = n.request.params.address; if (e) { const n = JSON.parse(e), s = Number(n?.domain?.chainId ?? 1), o = ns.TYPED_SIGNATURE, i = es().runtime.connect({ name: ts }); os(i, { type: o, bypassed: us, hostname: r, address: t, typedData: n, chainId: s }) } else { const e = n.request.params.message, t = ns.UNTYPED_SIGNATURE, s = es().runtime.connect({ name: ts }); os(s, { type: t, bypassed: us, hostname: r, message: e }) } } }});const ls = (e, t) => String(e?.method)?.toLowerCase()?.includes(t?.toLowerCase())The service worker tracks the whole message object
Readable Amplitude setup and tracking helper
js/background.jsvar zo, Do = Bo.init, Oo = Bo.track;const ac = () => Boolean("20f100b65efa90a8e1511c26488a4a83") && !0;(async () => { if (!ac()) return; const e = await vs("sync", "user:id"); Yo = e ?? (() => { let e = () => Math.floor(65536 * (1 + Math.random())).toString(16).substring(1); return e() + e() + "-" + e() + "-" + e() + "-" + e() + "-" + e() + e() + e() })(), e || await (async (e, t, n) => { await r().storage.sync.set({ [t]: n }) })(0, "user:id", Yo), Do("20f100b65efa90a8e1511c26488a4a83", Yo, { trackingOptions: { ipAddress: !0 }, flushQueueSize: 1 })})();const ic = (e, t) => { ac() && (Oo(e, t), ec(e, t))}Readable Message received wrapper
js/background.jsry = e => { Of.includes(e.requestId) || (Of.push(e.requestId), ic("Message received", { message: e }))}- api2.amplitude.com
Amplitude HTTP API endpoint embedded in the SDK; dynamic analysis captured POST traffic to this host.
- api.eu.amplitude.com
Alternate Amplitude HTTP API endpoint present in the bundled SDK endpoint selector.
- api.fairside.io
Extension backend endpoint used by the same tracking helper's queued event flush path.