harden: key remembered site permissions by full origin (closes #402)
allowedSites and deniedSites held the bare hostname, so a grant to https://dapp.example also authorised http://dapp.example and every port on that host, and the connection, transaction and signature prompts named only the hostname. Both lists now store and match the full origin (scheme://host[:port]), the key the connections approved without Remember already used. The prompts, the Settings site lists and AUTISTMASK_REMOVE_SITE use the origin too. Entries saved by hostname are not migrated (pre-1.0): they match no site. Model: opus-5-5
This commit was merged in pull request #431.
This commit is contained in:
@@ -1157,7 +1157,7 @@ each caught only by a reviewer re-deriving thirty fields by hand.
|
|||||||
The `allowedSites` case is why the entry check is not optional. A stored
|
The `allowedSites` case is why the entry check is not optional. A stored
|
||||||
`{"0x…": "notalist"}` is a well-formed object holding a malformed entry: it
|
`{"0x…": "notalist"}` is a well-formed object holding a malformed entry: it
|
||||||
passed the gate, rendered a completely healthy popup, and then threw inside
|
passed the gate, rendered a completely healthy popup, and then threw inside
|
||||||
`saveState()`'s per-hostname merge, so every save from that moment on failed and
|
`saveState()`'s per-origin merge, so every save from that moment on failed and
|
||||||
the user went on operating a wallet that was persisting nothing
|
the user went on operating a wallet that was persisting nothing
|
||||||
([#362](https://git.eeqj.de/sneak/AutistMask/issues/362)). A save that fails is
|
([#362](https://git.eeqj.de/sneak/AutistMask/issues/362)). A save that fails is
|
||||||
now also reported rather than swallowed: `onSaveFailure()` in
|
now also reported rather than swallowed: `onSaveFailure()` in
|
||||||
@@ -1631,13 +1631,13 @@ view would leave a wallet one click from deletion.
|
|||||||
a value carrying its unit, hex (`0x10`) or exponent (`1e3`) notation —
|
a value carrying its unit, hex (`0x10`) or exponent (`1e3`) notation —
|
||||||
is refused with a flash message and the field snaps back to the stored
|
is refused with a flash message and the field snaps back to the stored
|
||||||
threshold, so a number the user did not type is never stored.
|
threshold, so a number the user did not type is never stored.
|
||||||
- Allowed Sites: the hostnames remembered as allowed, under any address,
|
- Allowed Sites: the origins (scheme, host and port) remembered as allowed,
|
||||||
with remove buttons
|
under any address, with remove buttons
|
||||||
- Connected Sites: the hostnames of the sites allowed without "Remember my
|
- Connected Sites: the origins of the sites allowed without "Remember my
|
||||||
choice" that are still connected, with remove buttons. Only the background
|
choice" that are still connected, with remove buttons. Only the background
|
||||||
holds these, in memory, and Settings asks it for them with
|
holds these, in memory, and Settings asks it for them with
|
||||||
`AUTISTMASK_GET_CONNECTED_SITES`
|
`AUTISTMASK_GET_CONNECTED_SITES`
|
||||||
- Denied Sites: the hostnames remembered as denied, under any address, with
|
- Denied Sites: the origins remembered as denied, under any address, with
|
||||||
remove buttons
|
remove buttons
|
||||||
- About: project link, license, author, version, release date, and the
|
- About: project link, license, author, version, release date, and the
|
||||||
commit, which links to the commit in the repository
|
commit, which links to the commit in the repository
|
||||||
@@ -1651,10 +1651,11 @@ view would leave a wallet one click from deletion.
|
|||||||
- Tap wallet name → inline rename field (no screen change)
|
- Tap wallet name → inline rename field (no screen change)
|
||||||
- `[x]` on a tracked token → removes it in place (no screen change)
|
- `[x]` on a tracked token → removes it in place (no screen change)
|
||||||
- `[x]` on an allowed or connected site → disconnects that site, in place:
|
- `[x]` on an allowed or connected site → disconnects that site, in place:
|
||||||
its hostname is dropped from Allowed Sites under every address, and
|
its origin is dropped from Allowed Sites under every address, and
|
||||||
`AUTISTMASK_REMOVE_SITE` has the background end every connection approved
|
`AUTISTMASK_REMOVE_SITE` has the background end every connection approved
|
||||||
without "Remember" from an origin with that hostname, under any address,
|
without "Remember" from that origin, under any address, and send
|
||||||
and send `accountsChanged` with an empty list to the site's open tabs.
|
`accountsChanged` with an empty list to the open tabs of that origin. The
|
||||||
|
same host under another scheme or port is another site and is left alone.
|
||||||
Only the extension's own pages may send either message
|
Only the extension's own pages may send either message
|
||||||
- `[x]` on a denied site → forgets the refusal, in place; it connects
|
- `[x]` on a denied site → forgets the refusal, in place; it connects
|
||||||
nothing and tells the background nothing
|
nothing and tells the background nothing
|
||||||
@@ -1838,14 +1839,18 @@ view would leave a wallet one click from deletion.
|
|||||||
|
|
||||||
- **When**: A website requests wallet access via `eth_requestAccounts` or
|
- **When**: A website requests wallet access via `eth_requestAccounts` or
|
||||||
`wallet_requestPermissions` and is on neither the allowed nor the denied list.
|
`wallet_requestPermissions` and is on neither the allowed nor the denied list.
|
||||||
The background script prefers the toolbar popup (`action.openPopup()`) and
|
A site is its full origin, `scheme://host[:port]`, on both lists and for a
|
||||||
falls back to a separate popup window (`src/background/index.js`,
|
connection allowed without "Remember": a choice for `https://dapp.example`
|
||||||
|
says nothing about `http://dapp.example` or another port of that host. The
|
||||||
|
background script prefers the toolbar popup (`action.openPopup()`) and falls
|
||||||
|
back to a separate popup window (`src/background/index.js`,
|
||||||
`requestApproval()`).
|
`requestApproval()`).
|
||||||
- **Elements**:
|
- **Elements**:
|
||||||
- "Connection Request" heading
|
- "Connection Request" heading
|
||||||
- Phishing warning banner (shown when the hostname is on the phishing
|
- Phishing warning banner (shown when the hostname is on the phishing
|
||||||
blocklist)
|
blocklist)
|
||||||
- Site hostname (bold) + "wants to connect to your wallet"
|
- Site origin (bold, scheme and port included) + "wants to connect to your
|
||||||
|
wallet"
|
||||||
- Address that will be shared (color dot + full address + etherscan link)
|
- Address that will be shared (color dot + full address + etherscan link)
|
||||||
- "Remember my choice for this site" checkbox
|
- "Remember my choice for this site" checkbox
|
||||||
- "Allow" / "Deny" buttons
|
- "Allow" / "Deny" buttons
|
||||||
@@ -1875,7 +1880,8 @@ view would leave a wallet one click from deletion.
|
|||||||
- "Transaction Request" heading
|
- "Transaction Request" heading
|
||||||
- Phishing warning banner (shown when the hostname is on the phishing
|
- Phishing warning banner (shown when the hostname is on the phishing
|
||||||
blocklist)
|
blocklist)
|
||||||
- Site hostname (bold) + "wants to send a transaction"
|
- Site origin (bold, scheme and port included) + "wants to send a
|
||||||
|
transaction"
|
||||||
- Decoded action (if calldata is recognized): action name, token details,
|
- Decoded action (if calldata is recognized): action name, token details,
|
||||||
amounts, steps, deadline (see Transaction Decoding)
|
amounts, steps, deadline (see Transaction Decoding)
|
||||||
- From: color dot + full address + etherscan link
|
- From: color dot + full address + etherscan link
|
||||||
@@ -1906,7 +1912,8 @@ view would leave a wallet one click from deletion.
|
|||||||
- "Signature Request" heading
|
- "Signature Request" heading
|
||||||
- Phishing warning banner (shown when the hostname is on the phishing
|
- Phishing warning banner (shown when the hostname is on the phishing
|
||||||
blocklist)
|
blocklist)
|
||||||
- Site hostname (bold) + "wants you to sign a message"
|
- Site origin (bold, scheme and port included) + "wants you to sign a
|
||||||
|
message"
|
||||||
- Danger warning box (shown for `eth_sign`, which signs a raw hash)
|
- Danger warning box (shown for `eth_sign`, which signs a raw hash)
|
||||||
- Type: "Personal message" or "Typed data (EIP-712)"
|
- Type: "Personal message" or "Typed data (EIP-712)"
|
||||||
- From: color dot + full address + etherscan link
|
- From: color dot + full address + etherscan link
|
||||||
|
|||||||
@@ -45,6 +45,18 @@ but the review is broader than any of them.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-04: Remembered site permissions are held by full origin
|
||||||
|
([#402](https://git.eeqj.de/sneak/AutistMask/issues/402)). `allowedSites` and
|
||||||
|
`deniedSites` stored the hostname alone, so a grant to `https://dapp.example`
|
||||||
|
also authorised `http://dapp.example` and every port on that host, and the
|
||||||
|
prompts named only the hostname. Both lists now store and match the origin
|
||||||
|
(`scheme://host[:port]`), the key the connections approved without "Remember"
|
||||||
|
already used, in `src/background/index.js` and in Settings, whose site lists
|
||||||
|
and `AUTISTMASK_REMOVE_SITE` carry the origin too. The connection, transaction
|
||||||
|
and signature prompts show the origin. Entries saved by hostname before this
|
||||||
|
change are not migrated (pre-1.0): they match no site, and Settings lists them
|
||||||
|
until they are removed.
|
||||||
|
|
||||||
- 2026-10-04: A page's request is credited only to the site the browser says
|
- 2026-10-04: A page's request is credited only to the site the browser says
|
||||||
sent it ([#407](https://git.eeqj.de/sneak/AutistMask/issues/407)). Where the
|
sent it ([#407](https://git.eeqj.de/sneak/AutistMask/issues/407)). Where the
|
||||||
browser does not give the sender's origin (Firefox before 126), the background
|
browser does not give the sender's origin (Firefox before 126), the background
|
||||||
|
|||||||
+5
-3
@@ -285,11 +285,13 @@ not appear and may be permanently lost.
|
|||||||
AutistMask injects a standard `window.ethereum` provider (EIP-1193) into web
|
AutistMask injects a standard `window.ethereum` provider (EIP-1193) into web
|
||||||
pages. When a site requests access to your wallet:
|
pages. When a site requests access to your wallet:
|
||||||
|
|
||||||
1. A popup appears showing the site's hostname and the address that will be
|
1. A popup appears showing the site's origin (its scheme, host and port, for
|
||||||
shared.
|
example `https://app.example`) and the address that will be shared.
|
||||||
2. Click "Allow" to connect or "Deny" to reject.
|
2. Click "Allow" to connect or "Deny" to reject.
|
||||||
3. Optionally check "Remember my choice for this site" to skip the prompt next
|
3. Optionally check "Remember my choice for this site" to skip the prompt next
|
||||||
time.
|
time. The choice applies to that exact origin only: a choice remembered for
|
||||||
|
`https://app.example` does not cover `http://app.example` or another port of
|
||||||
|
the same host, which ask again.
|
||||||
|
|
||||||
When a connected site requests a transaction, a separate approval popup appears
|
When a connected site requests a transaction, a separate approval popup appears
|
||||||
showing the transaction details (from, to, value, data, network fee, network and
|
showing the transaction details (from, to, value, data, network fee, network and
|
||||||
|
|||||||
+36
-47
@@ -57,9 +57,13 @@ const windowsNs = windowsApi();
|
|||||||
const actionNs = actionApi();
|
const actionNs = actionApi();
|
||||||
|
|
||||||
// Connected sites (in-memory, non-persisted): { "origin:address": true }
|
// Connected sites (in-memory, non-persisted): { "origin:address": true }
|
||||||
|
//
|
||||||
|
// A site is its full origin (scheme://host[:port]), here and in the
|
||||||
|
// remembered allowedSites/deniedSites lists alike: a grant to
|
||||||
|
// https://dapp.example says nothing about http://dapp.example or another port.
|
||||||
const connectedSites = {};
|
const connectedSites = {};
|
||||||
|
|
||||||
// Pending approval requests: { id: { origin, hostname, resolve } }
|
// Pending approval requests: { id: { origin, resolve } }
|
||||||
const pendingApprovals = {};
|
const pendingApprovals = {};
|
||||||
|
|
||||||
// One transaction approval at a time, wallet-wide.
|
// One transaction approval at a time, wallet-wide.
|
||||||
@@ -459,10 +463,10 @@ async function openApprovalWindow(id) {
|
|||||||
|
|
||||||
// Open an approval popup and return a promise that resolves with the user decision.
|
// Open an approval popup and return a promise that resolves with the user decision.
|
||||||
// Prefers the browser-action popup (anchored to toolbar, no macOS Space switch).
|
// Prefers the browser-action popup (anchored to toolbar, no macOS Space switch).
|
||||||
function requestApproval(origin, hostname) {
|
function requestApproval(origin) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
pendingApprovals[id] = { id, origin, hostname, resolve };
|
pendingApprovals[id] = { id, origin, resolve };
|
||||||
|
|
||||||
if (actionNs && typeof actionNs.openPopup === "function") {
|
if (actionNs && typeof actionNs.openPopup === "function") {
|
||||||
actionNs.setPopup({
|
actionNs.setPopup({
|
||||||
@@ -495,13 +499,12 @@ function requestApproval(origin, hostname) {
|
|||||||
// screen never named.
|
// screen never named.
|
||||||
// `slot` is the transaction-approval slot its caller holds. Handing the
|
// `slot` is the transaction-approval slot its caller holds. Handing the
|
||||||
// approval's id to it is what makes retiring the approval free the slot.
|
// approval's id to it is what makes retiring the approval free the slot.
|
||||||
function requestTxApproval(origin, hostname, approvedTx, approvedFrom, slot) {
|
function requestTxApproval(origin, approvedTx, approvedFrom, slot) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
pendingApprovals[id] = {
|
pendingApprovals[id] = {
|
||||||
id,
|
id,
|
||||||
origin,
|
origin,
|
||||||
hostname,
|
|
||||||
approvedTx,
|
approvedTx,
|
||||||
approvedFrom,
|
approvedFrom,
|
||||||
resolve,
|
resolve,
|
||||||
@@ -517,13 +520,12 @@ function requestTxApproval(origin, hostname, approvedTx, approvedFrom, slot) {
|
|||||||
// Uses windows.create() directly because sign approvals are triggered programmatically
|
// Uses windows.create() directly because sign approvals are triggered programmatically
|
||||||
// (from a dApp RPC call), not from a user gesture, so action.openPopup() is
|
// (from a dApp RPC call), not from a user gesture, so action.openPopup() is
|
||||||
// unreliable in this context.
|
// unreliable in this context.
|
||||||
function requestSignApproval(origin, hostname, signParams, approvedFrom) {
|
function requestSignApproval(origin, signParams, approvedFrom) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
pendingApprovals[id] = {
|
pendingApprovals[id] = {
|
||||||
id,
|
id,
|
||||||
origin,
|
origin,
|
||||||
hostname,
|
|
||||||
signParams,
|
signParams,
|
||||||
approvedFrom,
|
approvedFrom,
|
||||||
resolve,
|
resolve,
|
||||||
@@ -601,11 +603,11 @@ runtime.onConnect.addListener((port) => {
|
|||||||
// in the worker — a balance refresh in flight, another site's approval — has
|
// in the worker — a balance refresh in flight, another site's approval — has
|
||||||
// gone on running the whole time. Loading here used to replace the very
|
// gone on running the whole time. Loading here used to replace the very
|
||||||
// objects that work was holding.
|
// objects that work was holding.
|
||||||
async function rememberSiteChoice(field, address, hostname) {
|
async function rememberSiteChoice(field, address, origin) {
|
||||||
await updateState((s) => {
|
await updateState((s) => {
|
||||||
if (!s[field][address]) s[field][address] = [];
|
if (!s[field][address]) s[field][address] = [];
|
||||||
if (!s[field][address].includes(hostname)) {
|
if (!s[field][address].includes(origin)) {
|
||||||
s[field][address].push(hostname);
|
s[field][address].push(origin);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -618,12 +620,11 @@ async function handleConnectionRequest(origin) {
|
|||||||
return { error: { message: "No accounts available" } };
|
return { error: { message: "No accounts available" } };
|
||||||
}
|
}
|
||||||
|
|
||||||
const hostname = extractHostname(origin);
|
|
||||||
const allowed = s.allowedSites[activeAddress] || [];
|
const allowed = s.allowedSites[activeAddress] || [];
|
||||||
const denied = s.deniedSites[activeAddress] || [];
|
const denied = s.deniedSites[activeAddress] || [];
|
||||||
|
|
||||||
// Check denied list
|
// Check denied list
|
||||||
if (denied.includes(hostname)) {
|
if (denied.includes(origin)) {
|
||||||
return {
|
return {
|
||||||
error: {
|
error: {
|
||||||
code: 4001,
|
code: 4001,
|
||||||
@@ -634,25 +635,25 @@ async function handleConnectionRequest(origin) {
|
|||||||
|
|
||||||
// Check allowed list or in-memory connected
|
// Check allowed list or in-memory connected
|
||||||
if (
|
if (
|
||||||
allowed.includes(hostname) ||
|
allowed.includes(origin) ||
|
||||||
connectedSites[origin + ":" + activeAddress]
|
connectedSites[origin + ":" + activeAddress]
|
||||||
) {
|
) {
|
||||||
return { result: [activeAddress] };
|
return { result: [activeAddress] };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open approval popup
|
// Open approval popup
|
||||||
const decision = await requestApproval(origin, hostname);
|
const decision = await requestApproval(origin);
|
||||||
|
|
||||||
if (decision.approved) {
|
if (decision.approved) {
|
||||||
if (decision.remember) {
|
if (decision.remember) {
|
||||||
await rememberSiteChoice("allowedSites", activeAddress, hostname);
|
await rememberSiteChoice("allowedSites", activeAddress, origin);
|
||||||
} else {
|
} else {
|
||||||
connectedSites[origin + ":" + activeAddress] = true;
|
connectedSites[origin + ":" + activeAddress] = true;
|
||||||
}
|
}
|
||||||
return { result: [activeAddress] };
|
return { result: [activeAddress] };
|
||||||
} else {
|
} else {
|
||||||
if (decision.remember) {
|
if (decision.remember) {
|
||||||
await rememberSiteChoice("deniedSites", activeAddress, hostname);
|
await rememberSiteChoice("deniedSites", activeAddress, origin);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
error: {
|
error: {
|
||||||
@@ -698,10 +699,9 @@ async function handleRpc(method, params, origin) {
|
|||||||
const s = await getState();
|
const s = await getState();
|
||||||
const activeAddress = activeAddressOf(s);
|
const activeAddress = activeAddressOf(s);
|
||||||
if (!activeAddress) return { result: [] };
|
if (!activeAddress) return { result: [] };
|
||||||
const hostname = extractHostname(origin);
|
|
||||||
const allowed = s.allowedSites[activeAddress] || [];
|
const allowed = s.allowedSites[activeAddress] || [];
|
||||||
if (
|
if (
|
||||||
allowed.includes(hostname) ||
|
allowed.includes(origin) ||
|
||||||
connectedSites[origin + ":" + activeAddress]
|
connectedSites[origin + ":" + activeAddress]
|
||||||
) {
|
) {
|
||||||
return { result: [activeAddress] };
|
return { result: [activeAddress] };
|
||||||
@@ -731,10 +731,9 @@ async function handleRpc(method, params, origin) {
|
|||||||
// [TESTNET] banner under a user who believed they were on Sepolia.
|
// [TESTNET] banner under a user who believed they were on Sepolia.
|
||||||
const s = await getState();
|
const s = await getState();
|
||||||
const activeAddress = activeAddressOf(s);
|
const activeAddress = activeAddressOf(s);
|
||||||
const hostname = extractHostname(origin);
|
|
||||||
const allowed = s.allowedSites[activeAddress] || [];
|
const allowed = s.allowedSites[activeAddress] || [];
|
||||||
if (
|
if (
|
||||||
!allowed.includes(hostname) &&
|
!allowed.includes(origin) &&
|
||||||
!connectedSites[origin + ":" + activeAddress]
|
!connectedSites[origin + ":" + activeAddress]
|
||||||
) {
|
) {
|
||||||
return { error: { code: 4100, message: "Unauthorized" } };
|
return { error: { code: 4100, message: "Unauthorized" } };
|
||||||
@@ -806,10 +805,9 @@ async function handleRpc(method, params, origin) {
|
|||||||
if (method === "wallet_getPermissions") {
|
if (method === "wallet_getPermissions") {
|
||||||
const s = await getState();
|
const s = await getState();
|
||||||
const activeAddress = activeAddressOf(s);
|
const activeAddress = activeAddressOf(s);
|
||||||
const hostname = extractHostname(origin);
|
|
||||||
const allowed = s.allowedSites[activeAddress] || [];
|
const allowed = s.allowedSites[activeAddress] || [];
|
||||||
const isConnected =
|
const isConnected =
|
||||||
allowed.includes(hostname) ||
|
allowed.includes(origin) ||
|
||||||
connectedSites[origin + ":" + activeAddress];
|
connectedSites[origin + ":" + activeAddress];
|
||||||
if (!isConnected || !activeAddress) {
|
if (!isConnected || !activeAddress) {
|
||||||
return { result: [] };
|
return { result: [] };
|
||||||
@@ -835,10 +833,9 @@ async function handleRpc(method, params, origin) {
|
|||||||
if (!activeAddress)
|
if (!activeAddress)
|
||||||
return { error: { message: "No accounts available" } };
|
return { error: { message: "No accounts available" } };
|
||||||
|
|
||||||
const hostname = extractHostname(origin);
|
|
||||||
const allowed = s.allowedSites[activeAddress] || [];
|
const allowed = s.allowedSites[activeAddress] || [];
|
||||||
if (
|
if (
|
||||||
!allowed.includes(hostname) &&
|
!allowed.includes(origin) &&
|
||||||
!connectedSites[origin + ":" + activeAddress]
|
!connectedSites[origin + ":" + activeAddress]
|
||||||
) {
|
) {
|
||||||
return { error: { code: 4100, message: "Unauthorized" } };
|
return { error: { code: 4100, message: "Unauthorized" } };
|
||||||
@@ -870,7 +867,6 @@ async function handleRpc(method, params, origin) {
|
|||||||
|
|
||||||
const decision = await requestSignApproval(
|
const decision = await requestSignApproval(
|
||||||
origin,
|
origin,
|
||||||
hostname,
|
|
||||||
signParams,
|
signParams,
|
||||||
activeAddress,
|
activeAddress,
|
||||||
);
|
);
|
||||||
@@ -884,10 +880,9 @@ async function handleRpc(method, params, origin) {
|
|||||||
if (!activeAddress)
|
if (!activeAddress)
|
||||||
return { error: { message: "No accounts available" } };
|
return { error: { message: "No accounts available" } };
|
||||||
|
|
||||||
const hostname = extractHostname(origin);
|
|
||||||
const allowed = s.allowedSites[activeAddress] || [];
|
const allowed = s.allowedSites[activeAddress] || [];
|
||||||
if (
|
if (
|
||||||
!allowed.includes(hostname) &&
|
!allowed.includes(origin) &&
|
||||||
!connectedSites[origin + ":" + activeAddress]
|
!connectedSites[origin + ":" + activeAddress]
|
||||||
) {
|
) {
|
||||||
return { error: { code: 4100, message: "Unauthorized" } };
|
return { error: { code: 4100, message: "Unauthorized" } };
|
||||||
@@ -905,7 +900,6 @@ async function handleRpc(method, params, origin) {
|
|||||||
}
|
}
|
||||||
const decision = await requestSignApproval(
|
const decision = await requestSignApproval(
|
||||||
origin,
|
origin,
|
||||||
hostname,
|
|
||||||
signParams,
|
signParams,
|
||||||
activeAddress,
|
activeAddress,
|
||||||
);
|
);
|
||||||
@@ -946,10 +940,9 @@ async function handleSendTransaction(params, origin) {
|
|||||||
const activeAddress = activeAddressOf(s);
|
const activeAddress = activeAddressOf(s);
|
||||||
if (!activeAddress) return { error: { message: "No accounts available" } };
|
if (!activeAddress) return { error: { message: "No accounts available" } };
|
||||||
|
|
||||||
const hostname = extractHostname(origin);
|
|
||||||
const allowed = s.allowedSites[activeAddress] || [];
|
const allowed = s.allowedSites[activeAddress] || [];
|
||||||
if (
|
if (
|
||||||
!allowed.includes(hostname) &&
|
!allowed.includes(origin) &&
|
||||||
!connectedSites[origin + ":" + activeAddress]
|
!connectedSites[origin + ":" + activeAddress]
|
||||||
) {
|
) {
|
||||||
return { error: { code: 4100, message: "Unauthorized" } };
|
return { error: { code: 4100, message: "Unauthorized" } };
|
||||||
@@ -1023,7 +1016,6 @@ async function handleSendTransaction(params, origin) {
|
|||||||
|
|
||||||
const decision = await requestTxApproval(
|
const decision = await requestTxApproval(
|
||||||
origin,
|
origin,
|
||||||
hostname,
|
|
||||||
approvedTx,
|
approvedTx,
|
||||||
activeAddress,
|
activeAddress,
|
||||||
slot,
|
slot,
|
||||||
@@ -1097,10 +1089,9 @@ async function broadcastAccountsChanged() {
|
|||||||
}
|
}
|
||||||
for (const tab of tabs) {
|
for (const tab of tabs) {
|
||||||
const origin = tab.url ? new URL(tab.url).origin : "";
|
const origin = tab.url ? new URL(tab.url).origin : "";
|
||||||
const hostname = extractHostname(origin);
|
|
||||||
const hasPermission =
|
const hasPermission =
|
||||||
activeAddress &&
|
activeAddress &&
|
||||||
(allowed.includes(hostname) ||
|
(allowed.includes(origin) ||
|
||||||
connectedSites[origin + ":" + activeAddress]);
|
connectedSites[origin + ":" + activeAddress]);
|
||||||
// Same as chainChanged above: a tab without our content script
|
// Same as chainChanged above: a tab without our content script
|
||||||
// rejects, and that is expected rather than a fault.
|
// rejects, and that is expected rather than a fault.
|
||||||
@@ -1113,7 +1104,7 @@ async function broadcastAccountsChanged() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tell every open tab of a site Settings removed that it has no account.
|
// Tell every open tab of a site Settings removed that it has no account.
|
||||||
async function broadcastSiteRemoved(hostname) {
|
async function broadcastSiteRemoved(origin) {
|
||||||
let tabs;
|
let tabs;
|
||||||
try {
|
try {
|
||||||
tabs = await tabsQuery({});
|
tabs = await tabsQuery({});
|
||||||
@@ -1121,7 +1112,7 @@ async function broadcastSiteRemoved(hostname) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const tab of tabs) {
|
for (const tab of tabs) {
|
||||||
if (!tab.url || extractHostname(tab.url) !== hostname) continue;
|
if (!tab.url || new URL(tab.url).origin !== origin) continue;
|
||||||
tabsSendMessage(tab.id, {
|
tabsSendMessage(tab.id, {
|
||||||
type: "AUTISTMASK_EVENT",
|
type: "AUTISTMASK_EVENT",
|
||||||
eventName: "accountsChanged",
|
eventName: "accountsChanged",
|
||||||
@@ -1348,10 +1339,7 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
if (msg.type === "AUTISTMASK_GET_APPROVAL") {
|
if (msg.type === "AUTISTMASK_GET_APPROVAL") {
|
||||||
const approval = pendingApprovals[msg.id];
|
const approval = pendingApprovals[msg.id];
|
||||||
if (approval) {
|
if (approval) {
|
||||||
const resp = {
|
const resp = { origin: approval.origin };
|
||||||
hostname: approval.hostname,
|
|
||||||
origin: approval.origin,
|
|
||||||
};
|
|
||||||
if (approval.type === "tx") {
|
if (approval.type === "tx") {
|
||||||
resp.type = "tx";
|
resp.type = "tx";
|
||||||
// The populated transaction, and the address it was raised
|
// The populated transaction, and the address it was raised
|
||||||
@@ -1366,7 +1354,9 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
resp.approvedFrom = approval.approvedFrom;
|
resp.approvedFrom = approval.approvedFrom;
|
||||||
}
|
}
|
||||||
// Flag if the requesting domain is on the phishing blocklist.
|
// Flag if the requesting domain is on the phishing blocklist.
|
||||||
resp.isPhishingDomain = isPhishingDomain(approval.hostname);
|
resp.isPhishingDomain = isPhishingDomain(
|
||||||
|
extractHostname(approval.origin),
|
||||||
|
);
|
||||||
sendResponse(resp);
|
sendResponse(resp);
|
||||||
} else {
|
} else {
|
||||||
sendResponse(null);
|
sendResponse(null);
|
||||||
@@ -1700,23 +1690,22 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
if (msg.type === "AUTISTMASK_GET_CONNECTED_SITES") {
|
if (msg.type === "AUTISTMASK_GET_CONNECTED_SITES") {
|
||||||
sendResponse(
|
sendResponse(
|
||||||
Object.keys(connectedSites).map((key) =>
|
Object.keys(connectedSites).map((key) =>
|
||||||
extractHostname(key.slice(0, key.lastIndexOf(":"))),
|
key.slice(0, key.lastIndexOf(":")),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Settings removed this site and has already dropped its remembered
|
// Settings removed this site (msg.origin) and has already dropped its
|
||||||
// entries. Its connections approved without "Remember" end here, under
|
// remembered entries. Its connections approved without "Remember" end
|
||||||
// every address, and its open tabs are told it has no account.
|
// here, under every address, and its open tabs are told it has no account.
|
||||||
if (msg.type === "AUTISTMASK_REMOVE_SITE") {
|
if (msg.type === "AUTISTMASK_REMOVE_SITE") {
|
||||||
for (const key of Object.keys(connectedSites)) {
|
for (const key of Object.keys(connectedSites)) {
|
||||||
const origin = key.slice(0, key.lastIndexOf(":"));
|
if (key.slice(0, key.lastIndexOf(":")) === msg.origin) {
|
||||||
if (extractHostname(origin) === msg.hostname) {
|
|
||||||
delete connectedSites[key];
|
delete connectedSites[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
broadcastSiteRemoved(msg.hostname);
|
broadcastSiteRemoved(msg.origin);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1561,7 +1561,7 @@
|
|||||||
with extreme caution.
|
with extreme caution.
|
||||||
</div>
|
</div>
|
||||||
<p class="mb-2">
|
<p class="mb-2">
|
||||||
<span id="approve-tx-hostname" class="font-bold"></span>
|
<span id="approve-tx-origin" class="font-bold"></span>
|
||||||
wants to send a transaction.
|
wants to send a transaction.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -1662,7 +1662,7 @@
|
|||||||
funds. Proceed with extreme caution.
|
funds. Proceed with extreme caution.
|
||||||
</div>
|
</div>
|
||||||
<p class="mb-2">
|
<p class="mb-2">
|
||||||
<span id="approve-sign-hostname" class="font-bold"></span>
|
<span id="approve-sign-origin" class="font-bold"></span>
|
||||||
wants you to sign a message.
|
wants you to sign a message.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -1740,7 +1740,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<p class="mb-2">
|
<p class="mb-2">
|
||||||
<span id="approve-hostname" class="font-bold"></span>
|
<span id="approve-origin" class="font-bold"></span>
|
||||||
wants to connect to your wallet.
|
wants to connect to your wallet.
|
||||||
</p>
|
</p>
|
||||||
<div class="text-xs text-muted mb-1">
|
<div class="text-xs text-muted mb-1">
|
||||||
|
|||||||
@@ -306,7 +306,7 @@ function showTxApproval(details) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
$("approve-tx-hostname").textContent = details.hostname;
|
$("approve-tx-origin").textContent = details.origin;
|
||||||
$("approve-tx-from").innerHTML = approvalAddressHtml(details.approvedFrom);
|
$("approve-tx-from").innerHTML = approvalAddressHtml(details.approvedFrom);
|
||||||
|
|
||||||
// Show token symbol next to contract address if known
|
// Show token symbol next to contract address if known
|
||||||
@@ -645,7 +645,7 @@ function showSignApproval(details) {
|
|||||||
pendingSignParams = sp;
|
pendingSignParams = sp;
|
||||||
pendingSignFrom = details.approvedFrom;
|
pendingSignFrom = details.approvedFrom;
|
||||||
|
|
||||||
$("approve-sign-hostname").textContent = details.hostname;
|
$("approve-sign-origin").textContent = details.origin;
|
||||||
$("approve-sign-from").innerHTML = approvalAddressHtml(
|
$("approve-sign-from").innerHTML = approvalAddressHtml(
|
||||||
details.approvedFrom,
|
details.approvedFrom,
|
||||||
);
|
);
|
||||||
@@ -732,7 +732,7 @@ async function show(id) {
|
|||||||
"approve-site-phishing-warning",
|
"approve-site-phishing-warning",
|
||||||
details.isPhishingDomain,
|
details.isPhishingDomain,
|
||||||
);
|
);
|
||||||
$("approve-hostname").textContent = details.hostname;
|
$("approve-origin").textContent = details.origin;
|
||||||
$("approve-address").innerHTML = approvalAddressHtml(state.activeAddress);
|
$("approve-address").innerHTML = approvalAddressHtml(state.activeAddress);
|
||||||
attachCopyHandlers("view-approve-site");
|
attachCopyHandlers("view-approve-site");
|
||||||
$("approve-remember").checked = state.rememberSiteChoice;
|
$("approve-remember").checked = state.rememberSiteChoice;
|
||||||
|
|||||||
+17
-17
@@ -34,35 +34,35 @@ const { notify, sendMessage } = require("../../shared/browserApi");
|
|||||||
let versionClickCount = 0;
|
let versionClickCount = 0;
|
||||||
let versionClickTimer = null;
|
let versionClickTimer = null;
|
||||||
|
|
||||||
// One row per hostname, however many addresses or origins it appears under,
|
// One row per site origin, however many addresses it appears under, each with
|
||||||
// each with an [x] that hands it to onRemove.
|
// an [x] that hands it to onRemove.
|
||||||
function renderSiteList(containerId, hostnames, onRemove) {
|
function renderSiteList(containerId, origins, onRemove) {
|
||||||
const container = $(containerId);
|
const container = $(containerId);
|
||||||
const unique = [...new Set(hostnames)];
|
const unique = [...new Set(origins)];
|
||||||
if (unique.length === 0) {
|
if (unique.length === 0) {
|
||||||
container.innerHTML = '<p class="text-xs text-muted">None</p>';
|
container.innerHTML = '<p class="text-xs text-muted">None</p>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let html = "";
|
let html = "";
|
||||||
unique.forEach((hostname) => {
|
unique.forEach((origin) => {
|
||||||
html += `<div class="flex justify-between items-center text-xs py-1 border-b border-border-light">`;
|
html += `<div class="flex justify-between items-center text-xs py-1 border-b border-border-light">`;
|
||||||
// A hostname the URL parser produced cannot carry a delimiter, so
|
// An origin the URL parser produced cannot carry a delimiter, so
|
||||||
// this is escaped for the rule rather than for a known hole — the
|
// this is escaped for the rule rather than for a known hole — the
|
||||||
// rule being that nothing reaches innerHTML unescaped.
|
// rule being that nothing reaches innerHTML unescaped.
|
||||||
html += `<span>${escapeHtml(hostname)}</span>`;
|
html += `<span>${escapeHtml(origin)}</span>`;
|
||||||
html += `<button class="btn-remove-site border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer" data-hostname="${escapeHtml(hostname)}">[x]</button>`;
|
html += `<button class="btn-remove-site border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer" data-origin="${escapeHtml(origin)}">[x]</button>`;
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
});
|
});
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
container.querySelectorAll(".btn-remove-site").forEach((btn) => {
|
container.querySelectorAll(".btn-remove-site").forEach((btn) => {
|
||||||
btn.addEventListener("click", () => onRemove(btn.dataset.hostname));
|
btn.addEventListener("click", () => onRemove(btn.dataset.origin));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop a hostname from a remembered site list under every address.
|
// Drop a site origin from a remembered site list under every address.
|
||||||
function forgetHostname(siteMap, hostname) {
|
function forgetOrigin(siteMap, origin) {
|
||||||
for (const addr of Object.keys(siteMap)) {
|
for (const addr of Object.keys(siteMap)) {
|
||||||
siteMap[addr] = siteMap[addr].filter((h) => h !== hostname);
|
siteMap[addr] = siteMap[addr].filter((o) => o !== origin);
|
||||||
if (siteMap[addr].length === 0) {
|
if (siteMap[addr].length === 0) {
|
||||||
delete siteMap[addr];
|
delete siteMap[addr];
|
||||||
}
|
}
|
||||||
@@ -72,16 +72,16 @@ function forgetHostname(siteMap, hostname) {
|
|||||||
// Removing a site from Allowed Sites or Connected Sites disconnects it: it is
|
// Removing a site from Allowed Sites or Connected Sites disconnects it: it is
|
||||||
// no longer allowed under any address, and the background ends its
|
// no longer allowed under any address, and the background ends its
|
||||||
// connections approved without "Remember" and tells its open tabs.
|
// connections approved without "Remember" and tells its open tabs.
|
||||||
async function removeAllowedSite(hostname) {
|
async function removeAllowedSite(origin) {
|
||||||
forgetHostname(state.allowedSites, hostname);
|
forgetOrigin(state.allowedSites, origin);
|
||||||
await saveState();
|
await saveState();
|
||||||
notify({ type: "AUTISTMASK_REMOVE_SITE", hostname });
|
notify({ type: "AUTISTMASK_REMOVE_SITE", origin });
|
||||||
await renderSiteLists();
|
await renderSiteLists();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removing a denied site only forgets the refusal; it connects nothing.
|
// Removing a denied site only forgets the refusal; it connects nothing.
|
||||||
async function removeDeniedSite(hostname) {
|
async function removeDeniedSite(origin) {
|
||||||
forgetHostname(state.deniedSites, hostname);
|
forgetOrigin(state.deniedSites, origin);
|
||||||
await saveState();
|
await saveState();
|
||||||
await renderSiteLists();
|
await renderSiteLists();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,11 +102,11 @@ function tokenRefs(value) {
|
|||||||
|
|
||||||
// A list of strings, for the fields whose entries are dereferenced as text:
|
// A list of strings, for the fields whose entries are dereferenced as text:
|
||||||
// fraudContracts (`a.toLowerCase()` in src/popup/views/send.js and
|
// fraudContracts (`a.toLowerCase()` in src/popup/views/send.js and
|
||||||
// src/shared/transactions.js) and each address's hostname list in the site maps
|
// src/shared/transactions.js) and each address's origin list in the site maps
|
||||||
// below (`h !== host` filters, `list.includes(hostname)` in the background).
|
// below (`o !== origin` filters, `list.includes(origin)` in the background).
|
||||||
//
|
//
|
||||||
// Same rule as tokenRefs(), for the same reason: the container AND the entries,
|
// Same rule as tokenRefs(), for the same reason: the container AND the entries,
|
||||||
// with a malformed entry DROPPED rather than repaired. A number in a hostname
|
// with a malformed entry DROPPED rather than repaired. A number in an origin
|
||||||
// list names no site and a number in fraudContracts names no contract, so there
|
// list names no site and a number in fraudContracts names no contract, so there
|
||||||
// is nothing to repair either to, and the empty list is a legitimate value that
|
// is nothing to repair either to, and the empty list is a legitimate value that
|
||||||
// survives. The result is a fresh array of primitives, so it shares no
|
// survives. The result is a fresh array of primitives, so it shares no
|
||||||
@@ -116,21 +116,22 @@ function textList(value) {
|
|||||||
return value.filter((entry) => typeof entry === "string");
|
return value.filter((entry) => typeof entry === "string");
|
||||||
}
|
}
|
||||||
|
|
||||||
// allowedSites / deniedSites: { [address]: [hostname, ...] }.
|
// allowedSites / deniedSites: { [address]: [origin, ...] }, each origin the
|
||||||
|
// full scheme://host[:port] of a site.
|
||||||
//
|
//
|
||||||
// The container check these had (truthy and not an array) is not the floor:
|
// The container check these had (truthy and not an array) is not the floor:
|
||||||
// `{"0xabc…": "notalist"}` IS a non-array object, and the dereference is one
|
// `{"0xabc…": "notalist"}` IS a non-array object, and the dereference is one
|
||||||
// level below it. saveState() merges these maps per key and then per hostname
|
// level below it. saveState() merges these maps per key and then per origin
|
||||||
// WITHIN each key, so a stored value that is not a list reaches `base.map()` in
|
// WITHIN each key, so a stored value that is not a list reaches `base.map()` in
|
||||||
// mergeListByIdentity() (src/shared/state.js) and throws — after the popup has
|
// mergeListByIdentity() (src/shared/state.js) and throws — after the popup has
|
||||||
// rendered, which is why every save from then on failed while the UI looked
|
// rendered, which is why every save from then on failed while the UI looked
|
||||||
// healthy (https://git.eeqj.de/sneak/AutistMask/issues/362). The Settings
|
// healthy (https://git.eeqj.de/sneak/AutistMask/issues/362). The Settings
|
||||||
// revoke button (`list.filter()`), and the background's
|
// revoke button (`list.filter()`), and the background's
|
||||||
// `allowed.includes(hostname)` gate, dereference it the same way; on that last
|
// `allowed.includes(origin)` gate, dereference it the same way; on that last
|
||||||
// one a stored string would also answer a SUBSTRING match, so a corrupt map
|
// one a stored string would also answer a SUBSTRING match, so a corrupt map
|
||||||
// could widen a site permission rather than merely throw.
|
// could widen a site permission rather than merely throw.
|
||||||
//
|
//
|
||||||
// An address key whose value is not a list of hostnames is dropped entirely: it
|
// An address key whose value is not a list of origins is dropped entirely: it
|
||||||
// grants and denies nothing, and dropping it fails closed. A stored own
|
// grants and denies nothing, and dropping it fails closed. A stored own
|
||||||
// "__proto__" key — which JSON can carry — is dropped for the same reason: it
|
// "__proto__" key — which JSON can carry — is dropped for the same reason: it
|
||||||
// can never be a wallet address, so it grants nothing either, and keeping it
|
// can never be a wallet address, so it grants nothing either, and keeping it
|
||||||
@@ -143,9 +144,9 @@ function siteMap(value) {
|
|||||||
if (!isRecord(value)) return out;
|
if (!isRecord(value)) return out;
|
||||||
for (const address of Object.keys(value)) {
|
for (const address of Object.keys(value)) {
|
||||||
if (address === "__proto__") continue;
|
if (address === "__proto__") continue;
|
||||||
const hostnames = textList(value[address]);
|
const origins = textList(value[address]);
|
||||||
if (hostnames.length === 0) continue;
|
if (origins.length === 0) continue;
|
||||||
defineOwn(out, address, hostnames);
|
defineOwn(out, address, origins);
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-10
@@ -304,7 +304,7 @@ function mergeAddress(base, ours, theirs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Merge a plain object keyed by string (allowedSites/deniedSites: address ->
|
// Merge a plain object keyed by string (allowedSites/deniedSites: address ->
|
||||||
// hostname list; networkEndpoints: networkId -> {rpcUrl, blockscoutUrl}) the
|
// origin list; networkEndpoints: networkId -> {rpcUrl, blockscoutUrl}) the
|
||||||
// same way mergeListByIdentity() merges an array — by key, not by whole-
|
// same way mergeListByIdentity() merges an array — by key, not by whole-
|
||||||
// object diff — so a key one page added or removed applies independently of
|
// object diff — so a key one page added or removed applies independently of
|
||||||
// a key another page edited. Unlike an array's identity function, an object
|
// a key another page edited. Unlike an array's identity function, an object
|
||||||
@@ -353,25 +353,25 @@ function mergeMapByKey(base, ours, theirs, mergeLeaf) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// allowedSites/deniedSites: { [address]: [hostname, ...] }. The hostname
|
// allowedSites/deniedSites: { [address]: [origin, ...] }. The origin
|
||||||
// list is itself membership, not a leaf — the background appends a newly
|
// list is itself membership, not a leaf — the background appends a newly
|
||||||
// approved/denied hostname to it, and the Settings "revoke" button
|
// approved/denied origin to it, and the Settings "revoke" button
|
||||||
// (src/popup/views/settings.js) filters a hostname out of it in place, from a
|
// (src/popup/views/settings.js) filters an origin out of it in place, from a
|
||||||
// different page. Merge it the same way wallets are merged: identity is the
|
// different page. Merge it the same way wallets are merged: identity is the
|
||||||
// hostname itself, so a merged pair is always equal and mergeItem is a no-op
|
// origin itself, so a merged pair is always equal and mergeItem is a no-op
|
||||||
// pick.
|
// pick.
|
||||||
function mergeHostnameList(base, ours, theirs) {
|
function mergeOriginList(base, ours, theirs) {
|
||||||
return mergeListByIdentity(
|
return mergeListByIdentity(
|
||||||
base,
|
base,
|
||||||
ours,
|
ours,
|
||||||
theirs,
|
theirs,
|
||||||
(hostname) => hostname,
|
(origin) => origin,
|
||||||
(b, o, t) => t,
|
(b, o, t) => t,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeSiteMap(base, ours, theirs) {
|
function mergeSiteMap(base, ours, theirs) {
|
||||||
return mergeMapByKey(base, ours, theirs, mergeHostnameList);
|
return mergeMapByKey(base, ours, theirs, mergeOriginList);
|
||||||
}
|
}
|
||||||
|
|
||||||
// networkEndpoints: { [networkId]: {rpcUrl, blockscoutUrl} }.
|
// networkEndpoints: { [networkId]: {rpcUrl, blockscoutUrl} }.
|
||||||
@@ -422,8 +422,8 @@ function mergeNetworkEndpoints(base, ours, theirs) {
|
|||||||
// address) apply independently instead of colliding as the same field.
|
// address) apply independently instead of colliding as the same field.
|
||||||
//
|
//
|
||||||
// `allowedSites` and `deniedSites` get the same treatment (mergeSiteMap(),
|
// `allowedSites` and `deniedSites` get the same treatment (mergeSiteMap(),
|
||||||
// by address key and then by hostname within each address's list), for the
|
// by address key and then by origin within each address's list), for the
|
||||||
// identical reason: the background appends a newly approved/denied hostname
|
// identical reason: the background appends a newly approved/denied origin
|
||||||
// to them, and the Settings "revoke" button (src/popup/views/settings.js)
|
// to them, and the Settings "revoke" button (src/popup/views/settings.js)
|
||||||
// filters one out in place, from a different page. A whole-field diff here
|
// filters one out in place, from a different page. A whole-field diff here
|
||||||
// doesn't just lose data, it is a security defect — a stale page's save can
|
// doesn't just lose data, it is a security defect — a stale page's save can
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
// The connection, transaction and signature prompts name the site by its full
|
||||||
|
// origin, scheme and port included, not by its bare hostname
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/402). A page served over http,
|
||||||
|
// or on another port, of a host the user trusts over https must not raise a
|
||||||
|
// prompt that reads as that trusted site.
|
||||||
|
//
|
||||||
|
// Driven against a minimal DOM stub in the shape
|
||||||
|
// tests/contractCreation.test.js uses.
|
||||||
|
|
||||||
|
globalThis.chrome = {
|
||||||
|
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const { state } = require("../src/shared/state");
|
||||||
|
const approval = require("../src/popup/views/approval");
|
||||||
|
|
||||||
|
// The site asking, in cleartext and on a port, which is what the hostname
|
||||||
|
// alone, dapp.example, used to hide.
|
||||||
|
const ORIGIN = "http://dapp.example:8080";
|
||||||
|
const FROM = "0x0000000000000000000000000000000000000a11";
|
||||||
|
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
||||||
|
|
||||||
|
function makeElement(id) {
|
||||||
|
const classes = new Set();
|
||||||
|
const el = {
|
||||||
|
id,
|
||||||
|
textContent: "",
|
||||||
|
value: "",
|
||||||
|
innerHTML: "",
|
||||||
|
disabled: false,
|
||||||
|
style: {},
|
||||||
|
dataset: {},
|
||||||
|
classList: {
|
||||||
|
add: (...names) => names.forEach((n) => classes.add(n)),
|
||||||
|
remove: (...names) => names.forEach((n) => classes.delete(n)),
|
||||||
|
contains: (n) => classes.has(n),
|
||||||
|
toggle: (n, force) => {
|
||||||
|
const on = force === undefined ? !classes.has(n) : force;
|
||||||
|
if (on) classes.add(n);
|
||||||
|
else classes.delete(n);
|
||||||
|
return on;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
addEventListener: () => {},
|
||||||
|
querySelectorAll: () => [],
|
||||||
|
appendChild: () => {},
|
||||||
|
};
|
||||||
|
// Views reach for .parentElement to hide whole sections.
|
||||||
|
Object.defineProperty(el, "parentElement", {
|
||||||
|
get: () => node(id + "-parent"),
|
||||||
|
});
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeDocument() {
|
||||||
|
const els = new Map();
|
||||||
|
return {
|
||||||
|
getElementById(id) {
|
||||||
|
// The debug banner is created on demand by helpers.js; absent
|
||||||
|
// is the state a non-debug, non-testnet popup is in.
|
||||||
|
if (id === "debug-banner") return null;
|
||||||
|
if (!els.has(id)) els.set(id, makeElement(id));
|
||||||
|
return els.get(id);
|
||||||
|
},
|
||||||
|
createElement: () => makeElement("created"),
|
||||||
|
body: { prepend: () => {} },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function node(id) {
|
||||||
|
return globalThis.document.getElementById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the prompt the background describes with `details`, the way the popup
|
||||||
|
// does: it asks for the approval and show() draws it.
|
||||||
|
async function openApproval(details) {
|
||||||
|
globalThis.document = makeDocument();
|
||||||
|
globalThis.window = { location: { search: "" } };
|
||||||
|
globalThis.chrome.runtime = {
|
||||||
|
connect: () => ({ postMessage: () => {} }),
|
||||||
|
sendMessage: (msg, reply) => {
|
||||||
|
if (!reply) return;
|
||||||
|
if (msg.type !== "AUTISTMASK_GET_APPROVAL") return reply(null);
|
||||||
|
reply({
|
||||||
|
origin: ORIGIN,
|
||||||
|
isPhishingDomain: false,
|
||||||
|
approvedFrom: FROM,
|
||||||
|
...details,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
approval.init({});
|
||||||
|
await approval.show(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
state.wallets = [];
|
||||||
|
state.activeAddress = FROM;
|
||||||
|
state.viewData = {};
|
||||||
|
state.viewStack = [];
|
||||||
|
state.currentView = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the connection prompt shows the origin", async () => {
|
||||||
|
await openApproval({});
|
||||||
|
expect(node("approve-origin").textContent).toBe(ORIGIN);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the transaction prompt shows the origin", async () => {
|
||||||
|
await openApproval({
|
||||||
|
type: "tx",
|
||||||
|
approvedTx: {
|
||||||
|
type: 2,
|
||||||
|
from: FROM,
|
||||||
|
chainId: "0x1",
|
||||||
|
nonce: "0x7",
|
||||||
|
gasLimit: "0x5208",
|
||||||
|
maxPriorityFeePerGas: "0x3b9aca00",
|
||||||
|
maxFeePerGas: "0x77359400",
|
||||||
|
to: RECIPIENT,
|
||||||
|
value: "0x0",
|
||||||
|
data: "0x",
|
||||||
|
accessList: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(node("approve-tx-origin").textContent).toBe(ORIGIN);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the signature prompt shows the origin", async () => {
|
||||||
|
await openApproval({
|
||||||
|
type: "sign",
|
||||||
|
// "Hello" as the hex a dApp passes to personal_sign.
|
||||||
|
signParams: {
|
||||||
|
method: "personal_sign",
|
||||||
|
message: "0x48656c6c6f",
|
||||||
|
from: FROM,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(node("approve-sign-origin").textContent).toBe(ORIGIN);
|
||||||
|
});
|
||||||
@@ -39,7 +39,6 @@ const other = new Wallet(OTHER_KEY);
|
|||||||
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
||||||
|
|
||||||
const ORIGIN = "https://dapp.example";
|
const ORIGIN = "https://dapp.example";
|
||||||
const HOSTNAME = "dapp.example";
|
|
||||||
// A page the wallet has never been connected to, whose requests are refused.
|
// A page the wallet has never been connected to, whose requests are refused.
|
||||||
const UNCONNECTED_ORIGIN = "https://stranger.example";
|
const UNCONNECTED_ORIGIN = "https://stranger.example";
|
||||||
const EXT_URL = "chrome-extension://autistmask/";
|
const EXT_URL = "chrome-extension://autistmask/";
|
||||||
@@ -199,7 +198,7 @@ function loadBackground(options) {
|
|||||||
networkId: "mainnet",
|
networkId: "mainnet",
|
||||||
rpcUrl: "https://rpc.invalid",
|
rpcUrl: "https://rpc.invalid",
|
||||||
activeAddress: signer.address,
|
activeAddress: signer.address,
|
||||||
allowedSites: { [signer.address]: [HOSTNAME] },
|
allowedSites: { [signer.address]: [ORIGIN] },
|
||||||
deniedSites: {},
|
deniedSites: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2193,12 +2192,54 @@ describe("removing an address ends a site's connection to it", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// A remembered permission belongs to the origin it was granted to, scheme and
|
||||||
|
// port included (https://git.eeqj.de/sneak/AutistMask/issues/402). The stored
|
||||||
|
// state allows ORIGIN, https://dapp.example. A cleartext page on the same host,
|
||||||
|
// which a network attacker can serve, and another port on it are other sites.
|
||||||
|
describe("a remembered permission is held by the full origin", () => {
|
||||||
|
for (const origin of ["http://dapp.example", "https://dapp.example:8443"]) {
|
||||||
|
test(`an https grant does not authorise ${origin}`, async () => {
|
||||||
|
const bg = loadBackground();
|
||||||
|
expect(await siteAccounts(bg, ORIGIN)).toEqual({
|
||||||
|
result: [signer.address],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await siteAccounts(bg, origin)).toEqual({ result: [] });
|
||||||
|
const send = bg.requestTx(TX_PARAMS, origin);
|
||||||
|
await settle();
|
||||||
|
expect(send.result()).toEqual({
|
||||||
|
error: { code: 4100, message: "Unauthorized" },
|
||||||
|
});
|
||||||
|
expect(send.id()).toBeNull();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("Remember stores the origin, so the cleartext page on that host is asked again", async () => {
|
||||||
|
const bg = loadBackground({ actionPopup: true });
|
||||||
|
const granted = bg.requestSite(FRESH_ORIGIN);
|
||||||
|
await settle();
|
||||||
|
const grantedId = granted.id();
|
||||||
|
bg.connectApproval(grantedId).decide(true, true);
|
||||||
|
await settle();
|
||||||
|
expect(granted.result()).toEqual({ result: [signer.address] });
|
||||||
|
expect(
|
||||||
|
bg.storage.read("autistmask").allowedSites[signer.address],
|
||||||
|
).toEqual([ORIGIN, FRESH_ORIGIN]);
|
||||||
|
|
||||||
|
const cleartext = bg.requestSite("http://fresh.example");
|
||||||
|
await settle();
|
||||||
|
// Unanswered: it is waiting on a prompt of its own.
|
||||||
|
expect(cleartext.result()).toBeNull();
|
||||||
|
expect(cleartext.id()).not.toBe(grantedId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Settings lists the sites allowed without "Remember", which only the
|
// Settings lists the sites allowed without "Remember", which only the
|
||||||
// background holds, and removing a site there, from either list, disconnects
|
// background holds, and removing a site there, from either list, disconnects
|
||||||
// it. These drive the real Settings view against the real background and
|
// it. These drive the real Settings view against the real background and
|
||||||
// click the [x] the user clicks.
|
// click the [x] the user clicks.
|
||||||
describe("removing a site in Settings disconnects it", () => {
|
describe("removing a site in Settings disconnects it", () => {
|
||||||
// FRESH_ORIGIN on another port, so its hostname is FRESH_ORIGIN's.
|
// The host of FRESH_ORIGIN on another port, which makes it another site.
|
||||||
const FRESH_OTHER_PORT = "https://fresh.example:8443";
|
const FRESH_OTHER_PORT = "https://fresh.example:8443";
|
||||||
|
|
||||||
// A site list's container. Its [x] buttons, data attributes and all, are
|
// A site list's container. Its [x] buttons, data attributes and all, are
|
||||||
@@ -2226,9 +2267,9 @@ describe("removing a site in Settings disconnects it", () => {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The hostnames a site list shows.
|
// The origins a site list shows.
|
||||||
function listed(list) {
|
function listed(list) {
|
||||||
return [...list.innerHTML.matchAll(/data-hostname="([^"]*)"/g)].map(
|
return [...list.innerHTML.matchAll(/data-origin="([^"]*)"/g)].map(
|
||||||
(match) => match[1],
|
(match) => match[1],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2259,10 +2300,10 @@ describe("removing a site in Settings disconnects it", () => {
|
|||||||
allowed: element("settings-allowed-sites"),
|
allowed: element("settings-allowed-sites"),
|
||||||
connected: element("settings-connected-sites"),
|
connected: element("settings-connected-sites"),
|
||||||
// Click the [x] beside a site, and let what it sends run.
|
// Click the [x] beside a site, and let what it sends run.
|
||||||
remove: async (list, hostname) => {
|
remove: async (list, origin) => {
|
||||||
expect(listed(list)).toContain(hostname);
|
expect(listed(list)).toContain(origin);
|
||||||
const button = list.buttons.find(
|
const button = list.buttons.find(
|
||||||
(b) => b.dataset.hostname === hostname,
|
(b) => b.dataset.origin === origin,
|
||||||
);
|
);
|
||||||
await button.click();
|
await button.click();
|
||||||
await settle();
|
await settle();
|
||||||
@@ -2274,14 +2315,15 @@ describe("removing a site in Settings disconnects it", () => {
|
|||||||
delete global.document;
|
delete global.document;
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Settings lists a site connected without Remember", async () => {
|
test("Settings lists each site by its origin", async () => {
|
||||||
const bg = loadBackground({ actionPopup: true });
|
const bg = loadBackground({ actionPopup: true });
|
||||||
await connect(bg, FRESH_ORIGIN, false);
|
await connect(bg, FRESH_ORIGIN, false);
|
||||||
|
await connect(bg, FRESH_OTHER_PORT, true);
|
||||||
|
|
||||||
const settings = await openSettings(bg);
|
const settings = await openSettings(bg);
|
||||||
|
|
||||||
expect(listed(settings.connected)).toEqual(["fresh.example"]);
|
expect(listed(settings.connected)).toEqual([FRESH_ORIGIN]);
|
||||||
expect(listed(settings.allowed)).toEqual([HOSTNAME]);
|
expect(listed(settings.allowed)).toEqual([ORIGIN, FRESH_OTHER_PORT]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("removing a site connected without Remember disconnects it and tells its tabs", async () => {
|
test("removing a site connected without Remember disconnects it and tells its tabs", async () => {
|
||||||
@@ -2293,6 +2335,7 @@ describe("removing a site in Settings disconnects it", () => {
|
|||||||
cb([
|
cb([
|
||||||
{ id: 1, url: FRESH_ORIGIN + "/app" },
|
{ id: 1, url: FRESH_ORIGIN + "/app" },
|
||||||
{ id: 2, url: ORIGIN + "/app" },
|
{ id: 2, url: ORIGIN + "/app" },
|
||||||
|
{ id: 3, url: FRESH_OTHER_PORT + "/app" },
|
||||||
]),
|
]),
|
||||||
sendMessage: (tabId, msg, cb) => {
|
sendMessage: (tabId, msg, cb) => {
|
||||||
sentToTabs.push({ tabId, msg });
|
sentToTabs.push({ tabId, msg });
|
||||||
@@ -2301,7 +2344,7 @@ describe("removing a site in Settings disconnects it", () => {
|
|||||||
};
|
};
|
||||||
const settings = await openSettings(bg);
|
const settings = await openSettings(bg);
|
||||||
|
|
||||||
await settings.remove(settings.connected, "fresh.example");
|
await settings.remove(settings.connected, FRESH_ORIGIN);
|
||||||
|
|
||||||
expect(await siteAccounts(bg)).toEqual({ result: [] });
|
expect(await siteAccounts(bg)).toEqual({ result: [] });
|
||||||
expect(sentToTabs).toEqual([
|
expect(sentToTabs).toEqual([
|
||||||
@@ -2321,20 +2364,45 @@ describe("removing a site in Settings disconnects it", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// The same hostname can hold both kinds of connection: one origin allowed
|
// One origin can hold both kinds of connection under two addresses:
|
||||||
// without Remember, then another, on a different port, allowed with it.
|
// remembered for one, allowed without Remember for the other.
|
||||||
test("removing a remembered site also ends its connection made without Remember", async () => {
|
test("removing a remembered site also ends its connection made without Remember", async () => {
|
||||||
|
const bg = loadBackground({ actionPopup: true });
|
||||||
|
const stored = bg.storage.read("autistmask");
|
||||||
|
stored.wallets[0].addresses.push({
|
||||||
|
address: other.address,
|
||||||
|
balance: "0",
|
||||||
|
tokenBalances: [],
|
||||||
|
});
|
||||||
|
bg.storage.write("autistmask", stored);
|
||||||
|
await connect(bg, FRESH_ORIGIN, true);
|
||||||
|
bg.setActiveAddress(other.address);
|
||||||
|
const pending = bg.requestSite(FRESH_ORIGIN);
|
||||||
|
await settle();
|
||||||
|
bg.connectApproval(pending.id()).decide(true, false);
|
||||||
|
await settle();
|
||||||
|
expect(pending.result()).toEqual({ result: [other.address] });
|
||||||
|
const settings = await openSettings(bg);
|
||||||
|
|
||||||
|
await settings.remove(settings.allowed, FRESH_ORIGIN);
|
||||||
|
|
||||||
|
expect(await siteAccounts(bg, FRESH_ORIGIN)).toEqual({ result: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("removing a remembered site leaves the same host on another port connected", async () => {
|
||||||
const bg = loadBackground({ actionPopup: true });
|
const bg = loadBackground({ actionPopup: true });
|
||||||
await connect(bg, FRESH_ORIGIN, false);
|
await connect(bg, FRESH_ORIGIN, false);
|
||||||
await connect(bg, FRESH_OTHER_PORT, true);
|
await connect(bg, FRESH_OTHER_PORT, true);
|
||||||
const settings = await openSettings(bg);
|
const settings = await openSettings(bg);
|
||||||
|
|
||||||
await settings.remove(settings.allowed, "fresh.example");
|
await settings.remove(settings.allowed, FRESH_OTHER_PORT);
|
||||||
|
|
||||||
expect(await siteAccounts(bg, FRESH_OTHER_PORT)).toEqual({
|
expect(await siteAccounts(bg, FRESH_OTHER_PORT)).toEqual({
|
||||||
result: [],
|
result: [],
|
||||||
});
|
});
|
||||||
expect(await siteAccounts(bg, FRESH_ORIGIN)).toEqual({ result: [] });
|
expect(await siteAccounts(bg, FRESH_ORIGIN)).toEqual({
|
||||||
|
result: [signer.address],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("a page can neither remove a site nor list the connected ones", async () => {
|
test("a page can neither remove a site nor list the connected ones", async () => {
|
||||||
@@ -2343,7 +2411,7 @@ describe("removing a site in Settings disconnects it", () => {
|
|||||||
const page = { url: FRESH_ORIGIN + "/index.html" };
|
const page = { url: FRESH_ORIGIN + "/index.html" };
|
||||||
|
|
||||||
const remove = bg.send(
|
const remove = bg.send(
|
||||||
{ type: "AUTISTMASK_REMOVE_SITE", hostname: "fresh.example" },
|
{ type: "AUTISTMASK_REMOVE_SITE", origin: FRESH_ORIGIN },
|
||||||
page,
|
page,
|
||||||
);
|
);
|
||||||
const list = bg.send({ type: "AUTISTMASK_GET_CONNECTED_SITES" }, page);
|
const list = bg.send({ type: "AUTISTMASK_GET_CONNECTED_SITES" }, page);
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ const signer = new Wallet(SIGNER_KEY);
|
|||||||
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
||||||
|
|
||||||
const CONNECTED_ORIGIN = "https://dapp.example";
|
const CONNECTED_ORIGIN = "https://dapp.example";
|
||||||
const CONNECTED_HOSTNAME = "dapp.example";
|
|
||||||
const EXT_URL = "chrome-extension://autistmask/";
|
const EXT_URL = "chrome-extension://autistmask/";
|
||||||
|
|
||||||
const MAINNET = networkById("mainnet");
|
const MAINNET = networkById("mainnet");
|
||||||
@@ -81,7 +80,7 @@ function storedProfile(networkId) {
|
|||||||
networkId,
|
networkId,
|
||||||
rpcUrl: net.defaultRpcUrl,
|
rpcUrl: net.defaultRpcUrl,
|
||||||
blockscoutUrl: net.defaultBlockscoutUrl,
|
blockscoutUrl: net.defaultBlockscoutUrl,
|
||||||
allowedSites: { [signer.address]: [CONNECTED_HOSTNAME] },
|
allowedSites: { [signer.address]: [CONNECTED_ORIGIN] },
|
||||||
deniedSites: {},
|
deniedSites: {},
|
||||||
trackedTokens: [],
|
trackedTokens: [],
|
||||||
lastBalanceRefresh: 0,
|
lastBalanceRefresh: 0,
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ const ADDRESS = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
|||||||
|
|
||||||
// The site the persisted state has connected, and one it has never heard of.
|
// The site the persisted state has connected, and one it has never heard of.
|
||||||
const CONNECTED_ORIGIN = "https://dapp.example";
|
const CONNECTED_ORIGIN = "https://dapp.example";
|
||||||
const CONNECTED_HOSTNAME = "dapp.example";
|
|
||||||
const STRANGER_ORIGIN = "https://stranger.example";
|
const STRANGER_ORIGIN = "https://stranger.example";
|
||||||
|
|
||||||
const MAINNET = networkById("mainnet");
|
const MAINNET = networkById("mainnet");
|
||||||
@@ -86,7 +85,7 @@ function loadBackground() {
|
|||||||
tokenHolderCache: {},
|
tokenHolderCache: {},
|
||||||
fraudContracts: [],
|
fraudContracts: [],
|
||||||
activeAddress: ADDRESS,
|
activeAddress: ADDRESS,
|
||||||
allowedSites: { [ADDRESS]: [CONNECTED_HOSTNAME] },
|
allowedSites: { [ADDRESS]: [CONNECTED_ORIGIN] },
|
||||||
deniedSites: {},
|
deniedSites: {},
|
||||||
};
|
};
|
||||||
const storage = makeStorageStub({ autistmask: persisted });
|
const storage = makeStorageStub({ autistmask: persisted });
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ const { networkById } = require("../src/shared/networks");
|
|||||||
const ADDRESS = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
const ADDRESS = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
||||||
|
|
||||||
const CONNECTED_ORIGIN = "https://dapp.example";
|
const CONNECTED_ORIGIN = "https://dapp.example";
|
||||||
const CONNECTED_HOSTNAME = "dapp.example";
|
|
||||||
const UNKNOWN_ORIGIN = "https://stranger.example";
|
const UNKNOWN_ORIGIN = "https://stranger.example";
|
||||||
|
|
||||||
const MAINNET = networkById("mainnet");
|
const MAINNET = networkById("mainnet");
|
||||||
@@ -41,7 +40,7 @@ function storedProfile(networkId) {
|
|||||||
networkId,
|
networkId,
|
||||||
rpcUrl: networkById(networkId).defaultRpcUrl,
|
rpcUrl: networkById(networkId).defaultRpcUrl,
|
||||||
blockscoutUrl: networkById(networkId).defaultBlockscoutUrl,
|
blockscoutUrl: networkById(networkId).defaultBlockscoutUrl,
|
||||||
allowedSites: { [ADDRESS]: [CONNECTED_HOSTNAME] },
|
allowedSites: { [ADDRESS]: [CONNECTED_ORIGIN] },
|
||||||
deniedSites: {},
|
deniedSites: {},
|
||||||
trackedTokens: [],
|
trackedTokens: [],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ const { makeStorageStub } = require("./support/storageStub");
|
|||||||
const ADDRESS = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
const ADDRESS = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
||||||
|
|
||||||
const CONNECTED_ORIGIN = "https://dapp.example";
|
const CONNECTED_ORIGIN = "https://dapp.example";
|
||||||
const CONNECTED_HOSTNAME = "dapp.example";
|
|
||||||
|
|
||||||
const MAINNET = networkById("mainnet");
|
const MAINNET = networkById("mainnet");
|
||||||
const SEPOLIA = networkById("sepolia");
|
const SEPOLIA = networkById("sepolia");
|
||||||
@@ -50,7 +49,7 @@ function storedProfile(networkId) {
|
|||||||
networkId,
|
networkId,
|
||||||
rpcUrl: CUSTOM_RPC,
|
rpcUrl: CUSTOM_RPC,
|
||||||
blockscoutUrl: CUSTOM_BLOCKSCOUT,
|
blockscoutUrl: CUSTOM_BLOCKSCOUT,
|
||||||
allowedSites: { [ADDRESS]: [CONNECTED_HOSTNAME] },
|
allowedSites: { [ADDRESS]: [CONNECTED_ORIGIN] },
|
||||||
deniedSites: {},
|
deniedSites: {},
|
||||||
trackedTokens: [{ address: TOKEN, symbol: "DAI", decimals: 18 }],
|
trackedTokens: [{ address: TOKEN, symbol: "DAI", decimals: 18 }],
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
@@ -168,7 +167,7 @@ describe("a chain switch on a worker that never loaded state", () => {
|
|||||||
expect(after.wallets).toEqual(walletFixture());
|
expect(after.wallets).toEqual(walletFixture());
|
||||||
expect(after.hasWallet).toBe(true);
|
expect(after.hasWallet).toBe(true);
|
||||||
expect(after.activeAddress).toBe(ADDRESS);
|
expect(after.activeAddress).toBe(ADDRESS);
|
||||||
expect(after.allowedSites).toEqual({ [ADDRESS]: [CONNECTED_HOSTNAME] });
|
expect(after.allowedSites).toEqual({ [ADDRESS]: [CONNECTED_ORIGIN] });
|
||||||
expect(after.trackedTokens).toEqual([
|
expect(after.trackedTokens).toEqual([
|
||||||
{ address: TOKEN, symbol: "DAI", decimals: 18 },
|
{ address: TOKEN, symbol: "DAI", decimals: 18 },
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ const signer = new Wallet(SIGNER_KEY);
|
|||||||
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
||||||
|
|
||||||
const CONNECTED_ORIGIN = "https://dapp.example";
|
const CONNECTED_ORIGIN = "https://dapp.example";
|
||||||
const CONNECTED_HOSTNAME = "dapp.example";
|
|
||||||
const EXT_URL = "chrome-extension://autistmask/";
|
const EXT_URL = "chrome-extension://autistmask/";
|
||||||
|
|
||||||
const SEPOLIA = networkById("sepolia");
|
const SEPOLIA = networkById("sepolia");
|
||||||
@@ -67,7 +66,7 @@ function storedProfile(networkId) {
|
|||||||
networkId,
|
networkId,
|
||||||
rpcUrl: net.defaultRpcUrl,
|
rpcUrl: net.defaultRpcUrl,
|
||||||
blockscoutUrl: net.defaultBlockscoutUrl,
|
blockscoutUrl: net.defaultBlockscoutUrl,
|
||||||
allowedSites: { [signer.address]: [CONNECTED_HOSTNAME] },
|
allowedSites: { [signer.address]: [CONNECTED_ORIGIN] },
|
||||||
deniedSites: {},
|
deniedSites: {},
|
||||||
trackedTokens: [],
|
trackedTokens: [],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ async function openTxApproval(to, data) {
|
|||||||
if (msg.type !== "AUTISTMASK_GET_APPROVAL") return reply(null);
|
if (msg.type !== "AUTISTMASK_GET_APPROVAL") return reply(null);
|
||||||
reply({
|
reply({
|
||||||
type: "tx",
|
type: "tx",
|
||||||
hostname: "dapp.example",
|
origin: "https://dapp.example",
|
||||||
isPhishingDomain: false,
|
isPhishingDomain: false,
|
||||||
approvedFrom: FROM,
|
approvedFrom: FROM,
|
||||||
approvedTx: {
|
approvedTx: {
|
||||||
|
|||||||
@@ -140,8 +140,14 @@ function load() {
|
|||||||
state.selectedWallet = 0;
|
state.selectedWallet = 0;
|
||||||
state.selectedAddress = 0;
|
state.selectedAddress = 0;
|
||||||
state.activeAddress = A0;
|
state.activeAddress = A0;
|
||||||
state.allowedSites = { [A0]: ["a.example"], [B0]: ["b.example"] };
|
state.allowedSites = {
|
||||||
state.deniedSites = { [B0]: ["c.example"], [C0]: ["d.example"] };
|
[A0]: ["https://a.example"],
|
||||||
|
[B0]: ["https://b.example"],
|
||||||
|
};
|
||||||
|
state.deniedSites = {
|
||||||
|
[B0]: ["https://c.example"],
|
||||||
|
[C0]: ["https://d.example"],
|
||||||
|
};
|
||||||
state.viewStack = ["main", "settings"];
|
state.viewStack = ["main", "settings"];
|
||||||
state.currentView = "settings";
|
state.currentView = "settings";
|
||||||
|
|
||||||
@@ -388,8 +394,8 @@ describe("deleting without the password", () => {
|
|||||||
await click("btn-delete-wallet-lost-confirm");
|
await click("btn-delete-wallet-lost-confirm");
|
||||||
|
|
||||||
const saved = (await storage.get("autistmask")).autistmask;
|
const saved = (await storage.get("autistmask")).autistmask;
|
||||||
expect(saved.allowedSites).toEqual({ [A0]: ["a.example"] });
|
expect(saved.allowedSites).toEqual({ [A0]: ["https://a.example"] });
|
||||||
expect(saved.deniedSites).toEqual({ [C0]: ["d.example"] });
|
expect(saved.deniedSites).toEqual({ [C0]: ["https://d.example"] });
|
||||||
});
|
});
|
||||||
|
|
||||||
// The route shares finishDelete() with the password route, so the
|
// The route shares finishDelete() with the password route, so the
|
||||||
@@ -437,7 +443,7 @@ describe("deleting without the password", () => {
|
|||||||
test("deleting the last wallet lands on Welcome with nothing left", async () => {
|
test("deleting the last wallet lands on Welcome with nothing left", async () => {
|
||||||
const { deleteWallet, state, storage } = load();
|
const { deleteWallet, state, storage } = load();
|
||||||
state.wallets = [wallet("Wallet 1", "secret-one", [A0])];
|
state.wallets = [wallet("Wallet 1", "secret-one", [A0])];
|
||||||
state.allowedSites = { [A0]: ["a.example"] };
|
state.allowedSites = { [A0]: ["https://a.example"] };
|
||||||
state.deniedSites = {};
|
state.deniedSites = {};
|
||||||
|
|
||||||
await openLostPassword(deleteWallet, 0);
|
await openLostPassword(deleteWallet, 0);
|
||||||
|
|||||||
@@ -438,11 +438,10 @@ step(
|
|||||||
await d.switchToWindow(popup);
|
await d.switchToWindow(popup);
|
||||||
await d.waitVisible("#view-approve-site");
|
await d.waitVisible("#view-approve-site");
|
||||||
|
|
||||||
const hostname = await d.text("#approve-hostname");
|
const origin = await d.text("#approve-origin");
|
||||||
assert(
|
assert(
|
||||||
hostname === "127.0.0.1",
|
origin === env.server.origin,
|
||||||
"the site prompt names the wrong origin: " +
|
"the site prompt names the wrong origin: " + JSON.stringify(origin),
|
||||||
JSON.stringify(hostname),
|
|
||||||
);
|
);
|
||||||
const shown = await d.text("#approve-address");
|
const shown = await d.text("#approve-address");
|
||||||
assert(
|
assert(
|
||||||
@@ -494,16 +493,16 @@ step(
|
|||||||
|
|
||||||
const screen = await d.execute(
|
const screen = await d.execute(
|
||||||
`return {
|
`return {
|
||||||
hostname: document.getElementById("approve-sign-hostname").textContent,
|
origin: document.getElementById("approve-sign-origin").textContent,
|
||||||
type: document.getElementById("approve-sign-type").textContent,
|
type: document.getElementById("approve-sign-type").textContent,
|
||||||
message: document.getElementById("approve-sign-message").textContent,
|
message: document.getElementById("approve-sign-message").textContent,
|
||||||
from: document.getElementById("approve-sign-from").textContent,
|
from: document.getElementById("approve-sign-from").textContent,
|
||||||
};`,
|
};`,
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
screen.hostname === "127.0.0.1",
|
screen.origin === env.server.origin,
|
||||||
"the sign prompt names the wrong origin: " +
|
"the sign prompt names the wrong origin: " +
|
||||||
JSON.stringify(screen.hostname),
|
JSON.stringify(screen.origin),
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
screen.type === "Personal message",
|
screen.type === "Personal message",
|
||||||
@@ -571,7 +570,7 @@ step(
|
|||||||
|
|
||||||
const screen = await d.execute(
|
const screen = await d.execute(
|
||||||
`return {
|
`return {
|
||||||
hostname: document.getElementById("approve-tx-hostname").textContent,
|
origin: document.getElementById("approve-tx-origin").textContent,
|
||||||
from: document.getElementById("approve-tx-from").textContent,
|
from: document.getElementById("approve-tx-from").textContent,
|
||||||
to: document.getElementById("approve-tx-to").textContent,
|
to: document.getElementById("approve-tx-to").textContent,
|
||||||
value: document.getElementById("approve-tx-value").textContent,
|
value: document.getElementById("approve-tx-value").textContent,
|
||||||
@@ -582,9 +581,9 @@ step(
|
|||||||
};`,
|
};`,
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
screen.hostname === "127.0.0.1",
|
screen.origin === env.server.origin,
|
||||||
"the transaction prompt names the wrong origin: " +
|
"the transaction prompt names the wrong origin: " +
|
||||||
JSON.stringify(screen.hostname),
|
JSON.stringify(screen.origin),
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
screen.from.toLowerCase().includes(env.address.toLowerCase()),
|
screen.from.toLowerCase().includes(env.address.toLowerCase()),
|
||||||
|
|||||||
+16
-19
@@ -35,6 +35,7 @@ const {
|
|||||||
const {
|
const {
|
||||||
DAPP_ORIGIN,
|
DAPP_ORIGIN,
|
||||||
DAPP_URL,
|
DAPP_URL,
|
||||||
|
PHISHING_DAPP_ORIGIN,
|
||||||
PHISHING_DAPP_URL,
|
PHISHING_DAPP_URL,
|
||||||
FEE_ESTIMATE_WEI,
|
FEE_ESTIMATE_WEI,
|
||||||
FEE_RESERVE_WEI,
|
FEE_RESERVE_WEI,
|
||||||
@@ -2471,8 +2472,6 @@ test("a token whose symbol() returns markup renders as text (#307)", async (env)
|
|||||||
// dApp, with real funds, against a real network. The RPC is stubbed
|
// dApp, with real funds, against a real network. The RPC is stubbed
|
||||||
// throughout. That pass stays on the human list before 1.0.0.
|
// throughout. That pass stays on the human list before 1.0.0.
|
||||||
|
|
||||||
const DAPP_HOSTNAME = new URL(DAPP_URL).hostname;
|
|
||||||
|
|
||||||
// The personal_sign payload. Sent as hex, which is what dApps send and what
|
// The personal_sign payload. Sent as hex, which is what dApps send and what
|
||||||
// the popup requires — it calls getBytes() on the message — and displayed on
|
// the popup requires — it calls getBytes() on the message — and displayed on
|
||||||
// the approval screen as the decoded text, which is what the user is agreeing
|
// the approval screen as the decoded text, which is what the user is agreeing
|
||||||
@@ -3016,11 +3015,10 @@ test("eth_requestAccounts rejected at the prompt returns a rejection (#183)", as
|
|||||||
try {
|
try {
|
||||||
await visible(popup, "#view-approve-site");
|
await visible(popup, "#view-approve-site");
|
||||||
|
|
||||||
const hostname = await popup.locator("#approve-hostname").innerText();
|
const origin = await popup.locator("#approve-origin").innerText();
|
||||||
assert(
|
assert(
|
||||||
hostname === DAPP_HOSTNAME,
|
origin === DAPP_ORIGIN,
|
||||||
"the site prompt names the wrong origin: " +
|
"the site prompt names the wrong origin: " + JSON.stringify(origin),
|
||||||
JSON.stringify(hostname),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// The control for the phishing test below: this origin is not on the
|
// The control for the phishing test below: this origin is not on the
|
||||||
@@ -3101,7 +3099,6 @@ test("a connect request from a blocklisted site is flagged (#219)", async (env)
|
|||||||
// check and the real approval screen. Nothing about the list is stubbed —
|
// check and the real approval screen. Nothing about the list is stubbed —
|
||||||
// there is nothing left to stub, since the extension no longer fetches it.
|
// there is nothing left to stub, since the extension no longer fetches it.
|
||||||
const phishingDapp = await openDapp(env.ctx, PHISHING_DAPP_URL);
|
const phishingDapp = await openDapp(env.ctx, PHISHING_DAPP_URL);
|
||||||
const hostname = new URL(PHISHING_DAPP_URL).hostname;
|
|
||||||
try {
|
try {
|
||||||
await reserveApprovalTab(env);
|
await reserveApprovalTab(env);
|
||||||
await startRequest(
|
await startRequest(
|
||||||
@@ -3114,15 +3111,15 @@ test("a connect request from a blocklisted site is flagged (#219)", async (env)
|
|||||||
try {
|
try {
|
||||||
await visible(popup, "#view-approve-site");
|
await visible(popup, "#view-approve-site");
|
||||||
|
|
||||||
const shown = await popup.locator("#approve-hostname").innerText();
|
const shown = await popup.locator("#approve-origin").innerText();
|
||||||
assert(
|
assert(
|
||||||
shown === hostname,
|
shown === PHISHING_DAPP_ORIGIN,
|
||||||
"the site prompt names the wrong origin: " +
|
"the site prompt names the wrong origin: " +
|
||||||
JSON.stringify(shown),
|
JSON.stringify(shown),
|
||||||
);
|
);
|
||||||
|
|
||||||
await visible(popup, "#approve-site-phishing-warning");
|
await visible(popup, "#approve-site-phishing-warning");
|
||||||
console.log("# phishing warning shown for " + hostname);
|
console.log("# phishing warning shown for " + PHISHING_DAPP_ORIGIN);
|
||||||
|
|
||||||
// Not remembered: a remembered decision for this origin would
|
// Not remembered: a remembered decision for this origin would
|
||||||
// outlive the test.
|
// outlive the test.
|
||||||
@@ -3152,15 +3149,15 @@ test("personal_sign signs, and the signature recovers to the address (#183)", as
|
|||||||
const boundary = await watchApprovalBoundary(popup, env);
|
const boundary = await watchApprovalBoundary(popup, env);
|
||||||
|
|
||||||
const screen = await popup.evaluate(() => ({
|
const screen = await popup.evaluate(() => ({
|
||||||
hostname: document.getElementById("approve-sign-hostname").textContent,
|
origin: document.getElementById("approve-sign-origin").textContent,
|
||||||
type: document.getElementById("approve-sign-type").textContent,
|
type: document.getElementById("approve-sign-type").textContent,
|
||||||
message: document.getElementById("approve-sign-message").textContent,
|
message: document.getElementById("approve-sign-message").textContent,
|
||||||
from: document.getElementById("approve-sign-from").textContent,
|
from: document.getElementById("approve-sign-from").textContent,
|
||||||
}));
|
}));
|
||||||
assert(
|
assert(
|
||||||
screen.hostname === DAPP_HOSTNAME,
|
screen.origin === DAPP_ORIGIN,
|
||||||
"the sign prompt names the wrong origin: " +
|
"the sign prompt names the wrong origin: " +
|
||||||
JSON.stringify(screen.hostname),
|
JSON.stringify(screen.origin),
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
screen.type === "Personal message",
|
screen.type === "Personal message",
|
||||||
@@ -3245,15 +3242,15 @@ test("eth_signTypedData_v4 signs, and the signature recovers (#183)", async (env
|
|||||||
const boundary = await watchApprovalBoundary(popup, env);
|
const boundary = await watchApprovalBoundary(popup, env);
|
||||||
|
|
||||||
const screen = await popup.evaluate(() => ({
|
const screen = await popup.evaluate(() => ({
|
||||||
hostname: document.getElementById("approve-sign-hostname").textContent,
|
origin: document.getElementById("approve-sign-origin").textContent,
|
||||||
type: document.getElementById("approve-sign-type").textContent,
|
type: document.getElementById("approve-sign-type").textContent,
|
||||||
message: document.getElementById("approve-sign-message").innerText,
|
message: document.getElementById("approve-sign-message").innerText,
|
||||||
from: document.getElementById("approve-sign-from").textContent,
|
from: document.getElementById("approve-sign-from").textContent,
|
||||||
}));
|
}));
|
||||||
assert(
|
assert(
|
||||||
screen.hostname === DAPP_HOSTNAME,
|
screen.origin === DAPP_ORIGIN,
|
||||||
"the typed data prompt names the wrong origin: " +
|
"the typed data prompt names the wrong origin: " +
|
||||||
JSON.stringify(screen.hostname),
|
JSON.stringify(screen.origin),
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
screen.type === "Typed data (EIP-712)",
|
screen.type === "Typed data (EIP-712)",
|
||||||
@@ -3351,7 +3348,7 @@ test("eth_sendTransaction signs the approved transaction and broadcasts it (#183
|
|||||||
const boundary = await watchApprovalBoundary(popup, env);
|
const boundary = await watchApprovalBoundary(popup, env);
|
||||||
|
|
||||||
const screen = await popup.evaluate(() => ({
|
const screen = await popup.evaluate(() => ({
|
||||||
hostname: document.getElementById("approve-tx-hostname").textContent,
|
origin: document.getElementById("approve-tx-origin").textContent,
|
||||||
from: document.getElementById("approve-tx-from").textContent,
|
from: document.getElementById("approve-tx-from").textContent,
|
||||||
to: document.getElementById("approve-tx-to").textContent,
|
to: document.getElementById("approve-tx-to").textContent,
|
||||||
value: document.getElementById("approve-tx-value").textContent,
|
value: document.getElementById("approve-tx-value").textContent,
|
||||||
@@ -3361,9 +3358,9 @@ test("eth_sendTransaction signs the approved transaction and broadcasts it (#183
|
|||||||
.classList.contains("hidden"),
|
.classList.contains("hidden"),
|
||||||
}));
|
}));
|
||||||
assert(
|
assert(
|
||||||
screen.hostname === DAPP_HOSTNAME,
|
screen.origin === DAPP_ORIGIN,
|
||||||
"the transaction prompt names the wrong origin: " +
|
"the transaction prompt names the wrong origin: " +
|
||||||
JSON.stringify(screen.hostname),
|
JSON.stringify(screen.origin),
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
screen.from.toLowerCase().includes(env.expectedAddress.toLowerCase()),
|
screen.from.toLowerCase().includes(env.expectedAddress.toLowerCase()),
|
||||||
|
|||||||
@@ -59,24 +59,32 @@ describe("the floor under allowedSites and deniedSites", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test(`an ${field} entry whose value is not a hostname list is dropped`, () => {
|
test(`an ${field} entry whose value is not an origin list is dropped`, () => {
|
||||||
for (const bad of ["dapp.example", 42, null, { a: 1 }, true]) {
|
for (const bad of [
|
||||||
|
"https://dapp.example",
|
||||||
|
42,
|
||||||
|
null,
|
||||||
|
{ a: 1 },
|
||||||
|
true,
|
||||||
|
]) {
|
||||||
expect(
|
expect(
|
||||||
normalizePersisted({ [field]: { [ADDRESS]: bad } })[field],
|
normalizePersisted({ [field]: { [ADDRESS]: bad } })[field],
|
||||||
).toEqual({});
|
).toEqual({});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test(`a hostname that is not text is dropped from an ${field} entry`, () => {
|
test(`an origin that is not text is dropped from an ${field} entry`, () => {
|
||||||
expect(
|
expect(
|
||||||
normalizePersisted({
|
normalizePersisted({
|
||||||
[field]: { [ADDRESS]: [42, null, "dapp.example", {}] },
|
[field]: {
|
||||||
|
[ADDRESS]: [42, null, "https://dapp.example", {}],
|
||||||
|
},
|
||||||
})[field],
|
})[field],
|
||||||
).toEqual({ [ADDRESS]: ["dapp.example"] });
|
).toEqual({ [ADDRESS]: ["https://dapp.example"] });
|
||||||
});
|
});
|
||||||
|
|
||||||
test(`a real ${field} map survives, copied not shared`, () => {
|
test(`a real ${field} map survives, copied not shared`, () => {
|
||||||
const saved = { [field]: { [ADDRESS]: ["dapp.example"] } };
|
const saved = { [field]: { [ADDRESS]: ["https://dapp.example"] } };
|
||||||
|
|
||||||
const out = normalizePersisted(saved);
|
const out = normalizePersisted(saved);
|
||||||
|
|
||||||
@@ -87,10 +95,13 @@ describe("the floor under allowedSites and deniedSites", () => {
|
|||||||
|
|
||||||
test(`a good ${field} entry beside a malformed one survives`, () => {
|
test(`a good ${field} entry beside a malformed one survives`, () => {
|
||||||
const out = normalizePersisted({
|
const out = normalizePersisted({
|
||||||
[field]: { [ADDRESS]: ["dapp.example"], [TOKEN_ADDRESS]: 42 },
|
[field]: {
|
||||||
|
[ADDRESS]: ["https://dapp.example"],
|
||||||
|
[TOKEN_ADDRESS]: 42,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(out[field]).toEqual({ [ADDRESS]: ["dapp.example"] });
|
expect(out[field]).toEqual({ [ADDRESS]: ["https://dapp.example"] });
|
||||||
});
|
});
|
||||||
|
|
||||||
test(`a stored own "__proto__" key in ${field} is dropped`, () => {
|
test(`a stored own "__proto__" key in ${field} is dropped`, () => {
|
||||||
@@ -100,7 +111,7 @@ describe("the floor under allowedSites and deniedSites", () => {
|
|||||||
// saveState()'s merge hands to the prototype setter on the next
|
// saveState()'s merge hands to the prototype setter on the next
|
||||||
// write.
|
// write.
|
||||||
const saved = JSON.parse(
|
const saved = JSON.parse(
|
||||||
'{"' + field + '":{"__proto__":["evil.invalid"]}}',
|
'{"' + field + '":{"__proto__":["https://evil.invalid"]}}',
|
||||||
);
|
);
|
||||||
|
|
||||||
const out = normalizePersisted(saved);
|
const out = normalizePersisted(saved);
|
||||||
@@ -197,7 +208,7 @@ describe("a malformed allowedSites entry", () => {
|
|||||||
const MALFORMED = [
|
const MALFORMED = [
|
||||||
{ name: "a string", value: "notalist" },
|
{ name: "a string", value: "notalist" },
|
||||||
{ name: "a number", value: 42 },
|
{ name: "a number", value: 42 },
|
||||||
{ name: "a record", value: { hostnames: ["dapp.example"] } },
|
{ name: "a record", value: { origins: ["https://dapp.example"] } },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const { name, value } of MALFORMED) {
|
for (const { name, value } of MALFORMED) {
|
||||||
@@ -231,7 +242,7 @@ describe("a malformed allowedSites entry", () => {
|
|||||||
const env = await bootPopup(
|
const env = await bootPopup(
|
||||||
unversionedValidProfile({
|
unversionedValidProfile({
|
||||||
allowedSites: {
|
allowedSites: {
|
||||||
[ADDRESS]: ["dapp.example"],
|
[ADDRESS]: ["https://dapp.example"],
|
||||||
[TOKEN_ADDRESS]: "notalist",
|
[TOKEN_ADDRESS]: "notalist",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -239,7 +250,7 @@ describe("a malformed allowedSites entry", () => {
|
|||||||
|
|
||||||
expect(env.pageErrors).toEqual([]);
|
expect(env.pageErrors).toEqual([]);
|
||||||
expect(env.storage.read("autistmask").allowedSites).toEqual({
|
expect(env.storage.read("autistmask").allowedSites).toEqual({
|
||||||
[ADDRESS]: ["dapp.example"],
|
[ADDRESS]: ["https://dapp.example"],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ const CONTRACT = [
|
|||||||
[ADDRESS],
|
[ADDRESS],
|
||||||
{ [ADDRESS]: 42 },
|
{ [ADDRESS]: 42 },
|
||||||
{ [ADDRESS]: [42, null, {}] },
|
{ [ADDRESS]: [42, null, {}] },
|
||||||
JSON.parse('{"__proto__":["evil.invalid"]}'),
|
JSON.parse('{"__proto__":["https://evil.invalid"]}'),
|
||||||
],
|
],
|
||||||
holds: siteMapHolds,
|
holds: siteMapHolds,
|
||||||
},
|
},
|
||||||
@@ -177,7 +177,7 @@ const CONTRACT = [
|
|||||||
[ADDRESS],
|
[ADDRESS],
|
||||||
{ [ADDRESS]: 42 },
|
{ [ADDRESS]: 42 },
|
||||||
{ [ADDRESS]: [42, null, {}] },
|
{ [ADDRESS]: [42, null, {}] },
|
||||||
JSON.parse('{"__proto__":["evil.invalid"]}'),
|
JSON.parse('{"__proto__":["https://evil.invalid"]}'),
|
||||||
],
|
],
|
||||||
holds: siteMapHolds,
|
holds: siteMapHolds,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ const ADDRESS = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
|||||||
|
|
||||||
// The site the persisted state has connected, and one it has never heard of.
|
// The site the persisted state has connected, and one it has never heard of.
|
||||||
const CONNECTED_ORIGIN = "https://dapp.example";
|
const CONNECTED_ORIGIN = "https://dapp.example";
|
||||||
const CONNECTED_HOSTNAME = "dapp.example";
|
|
||||||
const STRANGER_ORIGIN = "https://stranger.example";
|
const STRANGER_ORIGIN = "https://stranger.example";
|
||||||
|
|
||||||
async function settle() {
|
async function settle() {
|
||||||
@@ -58,7 +57,7 @@ function loadBackground() {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
activeAddress: ADDRESS,
|
activeAddress: ADDRESS,
|
||||||
allowedSites: { [ADDRESS]: [CONNECTED_HOSTNAME] },
|
allowedSites: { [ADDRESS]: [CONNECTED_ORIGIN] },
|
||||||
deniedSites: {},
|
deniedSites: {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ async function confirmSend(amount, token = "ETH") {
|
|||||||
async function approveTxWithFeePerGas(maxFeePerGas) {
|
async function approveTxWithFeePerGas(maxFeePerGas) {
|
||||||
approvalDetails = {
|
approvalDetails = {
|
||||||
type: "tx",
|
type: "tx",
|
||||||
hostname: "dapp.example",
|
origin: "https://dapp.example",
|
||||||
approvedFrom: HOLDER,
|
approvedFrom: HOLDER,
|
||||||
approvedTx: {
|
approvedTx: {
|
||||||
to: RECIPIENT,
|
to: RECIPIENT,
|
||||||
|
|||||||
+33
-31
@@ -270,31 +270,32 @@ describe("background refresh racing a wallet deleted on another page", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// allowedSites/deniedSites: { [address]: [hostname, ...] }. Mutated in place
|
// allowedSites/deniedSites: { [address]: [origin, ...] }. Mutated in place
|
||||||
// from two different contexts — src/background/index.js:592-599 pushes a
|
// from two different contexts — rememberSiteChoice() in
|
||||||
// newly approved hostname onto state.allowedSites[activeAddress], and the
|
// src/background/index.js pushes a newly approved origin onto
|
||||||
// Settings "revoke" button (src/popup/views/settings.js:55-68) filters a
|
// state.allowedSites[activeAddress], and the Settings "revoke" button
|
||||||
// hostname out of state[key][addr] in place, deleting the address key
|
// (forgetOrigin() in src/popup/views/settings.js) filters an origin out of
|
||||||
// entirely once its list is empty — the exact membership-vs-whole-field
|
// state[key][addr] in place, deleting the address key entirely once its list
|
||||||
// pattern that made the whole-field `wallets` diff unsafe, on a
|
// is empty — the exact membership-vs-whole-field pattern that made the
|
||||||
// security-relevant field: a stale whole-field save here can resurrect a
|
// whole-field `wallets` diff unsafe, on a security-relevant field: a stale
|
||||||
// revoked permission or wipe a freshly granted one.
|
// whole-field save here can resurrect a revoked permission or wipe a freshly
|
||||||
|
// granted one.
|
||||||
const ADDR1 = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
const ADDR1 = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
||||||
const ADDR2 = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
|
const ADDR2 = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
|
||||||
|
|
||||||
function approveSite(pageState, address, hostname) {
|
function approveSite(pageState, address, origin) {
|
||||||
if (!pageState.allowedSites[address]) {
|
if (!pageState.allowedSites[address]) {
|
||||||
pageState.allowedSites[address] = [];
|
pageState.allowedSites[address] = [];
|
||||||
}
|
}
|
||||||
if (!pageState.allowedSites[address].includes(hostname)) {
|
if (!pageState.allowedSites[address].includes(origin)) {
|
||||||
pageState.allowedSites[address].push(hostname);
|
pageState.allowedSites[address].push(origin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function revokeSite(pageState, hostname) {
|
function revokeSite(pageState, origin) {
|
||||||
for (const addr of Object.keys(pageState.allowedSites)) {
|
for (const addr of Object.keys(pageState.allowedSites)) {
|
||||||
pageState.allowedSites[addr] = pageState.allowedSites[addr].filter(
|
pageState.allowedSites[addr] = pageState.allowedSites[addr].filter(
|
||||||
(h) => h !== hostname,
|
(o) => o !== origin,
|
||||||
);
|
);
|
||||||
if (pageState.allowedSites[addr].length === 0) {
|
if (pageState.allowedSites[addr].length === 0) {
|
||||||
delete pageState.allowedSites[addr];
|
delete pageState.allowedSites[addr];
|
||||||
@@ -308,7 +309,7 @@ describe("a dApp approval racing a stale Settings page's later save", () => {
|
|||||||
await storage.set({
|
await storage.set({
|
||||||
autistmask: {
|
autistmask: {
|
||||||
wallets: [W1],
|
wallets: [W1],
|
||||||
allowedSites: { [ADDR2]: ["other.example"] },
|
allowedSites: { [ADDR2]: ["https://other.example"] },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -318,24 +319,24 @@ describe("a dApp approval racing a stale Settings page's later save", () => {
|
|||||||
await settings.state.loadState();
|
await settings.state.loadState();
|
||||||
|
|
||||||
// A dApp approval window, opened later, approves a new site for a
|
// A dApp approval window, opened later, approves a new site for a
|
||||||
// different address and saves — the real sequence at
|
// different address and saves — the real sequence in
|
||||||
// src/background/index.js:592-599.
|
// rememberSiteChoice(), src/background/index.js.
|
||||||
const approval = loadPage(storage);
|
const approval = loadPage(storage);
|
||||||
await approval.state.loadState();
|
await approval.state.loadState();
|
||||||
approveSite(approval.state.state, ADDR1, "dapp.example");
|
approveSite(approval.state.state, ADDR1, "https://dapp.example");
|
||||||
await approval.state.saveState();
|
await approval.state.saveState();
|
||||||
expect(
|
expect(
|
||||||
(await storage.get("autistmask")).autistmask.allowedSites[ADDR1],
|
(await storage.get("autistmask")).autistmask.allowedSites[ADDR1],
|
||||||
).toEqual(["dapp.example"]);
|
).toEqual(["https://dapp.example"]);
|
||||||
|
|
||||||
// Settings revokes its own, unrelated site — the real sequence at
|
// Settings revokes its own, unrelated site — the real sequence in
|
||||||
// src/popup/views/settings.js:55-68 — and saves from state loaded
|
// forgetOrigin(), src/popup/views/settings.js — and saves from state
|
||||||
// before the dApp approval ever happened.
|
// loaded before the dApp approval ever happened.
|
||||||
revokeSite(settings.state.state, "other.example");
|
revokeSite(settings.state.state, "https://other.example");
|
||||||
await settings.state.saveState();
|
await settings.state.saveState();
|
||||||
|
|
||||||
const persisted = (await storage.get("autistmask")).autistmask;
|
const persisted = (await storage.get("autistmask")).autistmask;
|
||||||
expect(persisted.allowedSites[ADDR1]).toEqual(["dapp.example"]);
|
expect(persisted.allowedSites[ADDR1]).toEqual(["https://dapp.example"]);
|
||||||
expect(persisted.allowedSites[ADDR2]).toBeUndefined();
|
expect(persisted.allowedSites[ADDR2]).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -346,7 +347,7 @@ describe("a revoked site permission against a stale page's later save", () => {
|
|||||||
await storage.set({
|
await storage.set({
|
||||||
autistmask: {
|
autistmask: {
|
||||||
wallets: [W1],
|
wallets: [W1],
|
||||||
allowedSites: { [ADDR1]: ["evil.example"] },
|
allowedSites: { [ADDR1]: ["https://evil.example"] },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -354,23 +355,24 @@ describe("a revoked site permission against a stale page's later save", () => {
|
|||||||
const stale = loadPage(storage);
|
const stale = loadPage(storage);
|
||||||
await stale.state.loadState();
|
await stale.state.loadState();
|
||||||
|
|
||||||
// Settings revokes it — src/popup/views/settings.js:55-68 — from a
|
// Settings revokes it — forgetOrigin(), src/popup/views/settings.js —
|
||||||
// second page.
|
// from a second page.
|
||||||
const settings = loadPage(storage);
|
const settings = loadPage(storage);
|
||||||
await settings.state.loadState();
|
await settings.state.loadState();
|
||||||
revokeSite(settings.state.state, "evil.example");
|
revokeSite(settings.state.state, "https://evil.example");
|
||||||
await settings.state.saveState();
|
await settings.state.saveState();
|
||||||
expect(
|
expect(
|
||||||
(await storage.get("autistmask")).autistmask.allowedSites[ADDR1],
|
(await storage.get("autistmask")).autistmask.allowedSites[ADDR1],
|
||||||
).toBeUndefined();
|
).toBeUndefined();
|
||||||
|
|
||||||
// The stale page, unaware of the revoke, approves an unrelated site
|
// The stale page, unaware of the revoke, approves an unrelated site
|
||||||
// for a different address and saves — src/background/index.js:592-599.
|
// for a different address and saves — rememberSiteChoice(),
|
||||||
approveSite(stale.state.state, ADDR2, "good.example");
|
// src/background/index.js.
|
||||||
|
approveSite(stale.state.state, ADDR2, "https://good.example");
|
||||||
await stale.state.saveState();
|
await stale.state.saveState();
|
||||||
|
|
||||||
const persisted = (await storage.get("autistmask")).autistmask;
|
const persisted = (await storage.get("autistmask")).autistmask;
|
||||||
expect(persisted.allowedSites[ADDR2]).toEqual(["good.example"]);
|
expect(persisted.allowedSites[ADDR2]).toEqual(["https://good.example"]);
|
||||||
expect(persisted.allowedSites[ADDR1]).toBeUndefined();
|
expect(persisted.allowedSites[ADDR1]).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -167,7 +167,9 @@ describe("an unversioned profile that is perfectly valid", () => {
|
|||||||
expect(stored.wallets[0].encryptedSecret).toBe("encrypted-secret-1");
|
expect(stored.wallets[0].encryptedSecret).toBe("encrypted-secret-1");
|
||||||
expect(stored.wallets[0].addresses[0].address).toBe(ADDRESS);
|
expect(stored.wallets[0].addresses[0].address).toBe(ADDRESS);
|
||||||
expect(stored.activeAddress).toBe(ADDRESS);
|
expect(stored.activeAddress).toBe(ADDRESS);
|
||||||
expect(stored.allowedSites).toEqual({ [ADDRESS]: ["dapp.example"] });
|
expect(stored.allowedSites).toEqual({
|
||||||
|
[ADDRESS]: ["https://dapp.example"],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ function unversionedValidProfile(extra) {
|
|||||||
networkId: "mainnet",
|
networkId: "mainnet",
|
||||||
rpcUrl: "https://ethereum-rpc.publicnode.com",
|
rpcUrl: "https://ethereum-rpc.publicnode.com",
|
||||||
blockscoutUrl: "https://eth.blockscout.com/api/v2",
|
blockscoutUrl: "https://eth.blockscout.com/api/v2",
|
||||||
allowedSites: { [ADDRESS]: ["dapp.example"] },
|
allowedSites: { [ADDRESS]: ["https://dapp.example"] },
|
||||||
deniedSites: {},
|
deniedSites: {},
|
||||||
trackedTokens: [],
|
trackedTokens: [],
|
||||||
theme: "system",
|
theme: "system",
|
||||||
|
|||||||
@@ -471,7 +471,7 @@ async function openSignScreen(data) {
|
|||||||
if (msg.type !== "AUTISTMASK_GET_APPROVAL") return reply(null);
|
if (msg.type !== "AUTISTMASK_GET_APPROVAL") return reply(null);
|
||||||
reply({
|
reply({
|
||||||
type: "sign",
|
type: "sign",
|
||||||
hostname: "dapp.example",
|
origin: "https://dapp.example",
|
||||||
isPhishingDomain: false,
|
isPhishingDomain: false,
|
||||||
approvedFrom: OWNER,
|
approvedFrom: OWNER,
|
||||||
signParams: request(data),
|
signParams: request(data),
|
||||||
|
|||||||
+20
-11
@@ -28,11 +28,14 @@ function makeState(overrides = {}) {
|
|||||||
selectedAddress: 0,
|
selectedAddress: 0,
|
||||||
activeAddress: A0,
|
activeAddress: A0,
|
||||||
allowedSites: {
|
allowedSites: {
|
||||||
[A0]: ["a.example"],
|
[A0]: ["https://a.example"],
|
||||||
[A1]: ["b.example"],
|
[A1]: ["https://b.example"],
|
||||||
[B0]: ["c.example"],
|
[B0]: ["https://c.example"],
|
||||||
|
},
|
||||||
|
deniedSites: {
|
||||||
|
[A1]: ["https://d.example"],
|
||||||
|
[C0]: ["https://e.example"],
|
||||||
},
|
},
|
||||||
deniedSites: { [A1]: ["d.example"], [C0]: ["e.example"] },
|
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -41,7 +44,7 @@ describe("removeWalletFromState", () => {
|
|||||||
test("deleting the last wallet clears hasWallet", () => {
|
test("deleting the last wallet clears hasWallet", () => {
|
||||||
const state = makeState({
|
const state = makeState({
|
||||||
wallets: [wallet("A", [A0])],
|
wallets: [wallet("A", [A0])],
|
||||||
allowedSites: { [A0]: ["a.example"] },
|
allowedSites: { [A0]: ["https://a.example"] },
|
||||||
deniedSites: {},
|
deniedSites: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -109,8 +112,8 @@ describe("removeWalletFromState", () => {
|
|||||||
|
|
||||||
removeWalletFromState(state, 0);
|
removeWalletFromState(state, 0);
|
||||||
|
|
||||||
expect(state.allowedSites).toEqual({ [B0]: ["c.example"] });
|
expect(state.allowedSites).toEqual({ [B0]: ["https://c.example"] });
|
||||||
expect(state.deniedSites).toEqual({ [C0]: ["e.example"] });
|
expect(state.deniedSites).toEqual({ [C0]: ["https://e.example"] });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -126,8 +129,14 @@ function makeAddressState(overrides = {}) {
|
|||||||
selectedWallet: 0,
|
selectedWallet: 0,
|
||||||
selectedAddress: 0,
|
selectedAddress: 0,
|
||||||
activeAddress: A0,
|
activeAddress: A0,
|
||||||
allowedSites: { [A0]: ["a.example"], [A1]: ["b.example"] },
|
allowedSites: {
|
||||||
deniedSites: { [A1]: ["d.example"], [B0]: ["e.example"] },
|
[A0]: ["https://a.example"],
|
||||||
|
[A1]: ["https://b.example"],
|
||||||
|
},
|
||||||
|
deniedSites: {
|
||||||
|
[A1]: ["https://d.example"],
|
||||||
|
[B0]: ["https://e.example"],
|
||||||
|
},
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -273,8 +282,8 @@ describe("removeAddressFromState", () => {
|
|||||||
|
|
||||||
removeAddressFromState(state, 0, 1);
|
removeAddressFromState(state, 0, 1);
|
||||||
|
|
||||||
expect(state.allowedSites).toEqual({ [A0]: ["a.example"] });
|
expect(state.allowedSites).toEqual({ [A0]: ["https://a.example"] });
|
||||||
expect(state.deniedSites).toEqual({ [B0]: ["e.example"] });
|
expect(state.deniedSites).toEqual({ [B0]: ["https://e.example"] });
|
||||||
});
|
});
|
||||||
|
|
||||||
// The derivation counter is a high-water mark, never rewound: "+" derives
|
// The derivation counter is a high-water mark, never rewound: "+" derives
|
||||||
|
|||||||
Reference in New Issue
Block a user