harden: list and end site connections made without Remember in Settings (closes #406)
A site allowed without "Remember" lives only in the background's in-memory connectedSites map. Settings never listed it, and AUTISTMASK_REMOVE_SITE, sent on every remove, did nothing, so the user could not end such a connection. Settings now asks the background for those sites and lists them under Connected Sites. Removing a site from Allowed Sites or Connected Sites drops its remembered entry under every address and sends AUTISTMASK_REMOVE_SITE with the hostname; the background deletes every matching connectedSites entry and sends accountsChanged with an empty list to the site's tabs. Only the extension's own pages may send either message. Model: opus-5-5
This commit is contained in:
@@ -2101,6 +2101,16 @@ describe("a site connection decided as the popup closes", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// What a site is told when it asks which account it may use.
|
||||
async function siteAccounts(bg, origin) {
|
||||
const { sendResponse } = bg.send(
|
||||
{ type: "AUTISTMASK_RPC", method: "eth_accounts", params: [] },
|
||||
{ origin: origin || FRESH_ORIGIN },
|
||||
);
|
||||
await settle();
|
||||
return sendResponse.mock.calls[0][0];
|
||||
}
|
||||
|
||||
// A site connected without "Remember" is held only in the background's memory,
|
||||
// keyed to the address it was connected to. Removing that address, or the
|
||||
// wallet holding it, must end the connection as part of the removal itself.
|
||||
@@ -2136,16 +2146,6 @@ describe("removing an address ends a site's connection to it", () => {
|
||||
return bg;
|
||||
}
|
||||
|
||||
// What FRESH_ORIGIN is told when it asks which account it may use.
|
||||
async function siteAccounts(bg) {
|
||||
const { sendResponse } = bg.send(
|
||||
{ type: "AUTISTMASK_RPC", method: "eth_accounts", params: [] },
|
||||
{ origin: FRESH_ORIGIN },
|
||||
);
|
||||
await settle();
|
||||
return sendResponse.mock.calls[0][0];
|
||||
}
|
||||
|
||||
test("removing the connected address ends the connection", async () => {
|
||||
const bg = await connectedBackground();
|
||||
expect(await siteAccounts(bg)).toEqual({ result: [signer.address] });
|
||||
@@ -2192,3 +2192,168 @@ describe("removing an address ends a site's connection to it", () => {
|
||||
expect(await siteAccounts(bg)).toEqual({ result: [signer.address] });
|
||||
});
|
||||
});
|
||||
|
||||
// Settings lists the sites allowed without "Remember", which only the
|
||||
// background holds, and removing a site there, from either list, disconnects
|
||||
// it. These drive the real Settings view against the real background and
|
||||
// click the [x] the user clicks.
|
||||
describe("removing a site in Settings disconnects it", () => {
|
||||
// FRESH_ORIGIN on another port, so its hostname is FRESH_ORIGIN's.
|
||||
const FRESH_OTHER_PORT = "https://fresh.example:8443";
|
||||
|
||||
// A site list's container. Its [x] buttons, data attributes and all, are
|
||||
// read back out of the rows the view wrote into it, so clicking one runs
|
||||
// the handler the view attached to it.
|
||||
function fakeSiteList() {
|
||||
const list = {
|
||||
innerHTML: "",
|
||||
buttons: [],
|
||||
querySelectorAll() {
|
||||
const tags = list.innerHTML.match(/<button[^>]*>/g) || [];
|
||||
list.buttons = tags.map((tag) => ({
|
||||
dataset: Object.fromEntries(
|
||||
[...tag.matchAll(/data-(\w+)="([^"]*)"/g)].map(
|
||||
(match) => [match[1], match[2]],
|
||||
),
|
||||
),
|
||||
addEventListener(event, handler) {
|
||||
this[event] = handler;
|
||||
},
|
||||
}));
|
||||
return list.buttons;
|
||||
},
|
||||
};
|
||||
return list;
|
||||
}
|
||||
|
||||
// The hostnames a site list shows.
|
||||
function listed(list) {
|
||||
return [...list.innerHTML.matchAll(/data-hostname="([^"]*)"/g)].map(
|
||||
(match) => match[1],
|
||||
);
|
||||
}
|
||||
|
||||
// A site connected the way the user does it, in the approval popup.
|
||||
async function connect(bg, origin, remember) {
|
||||
const pending = bg.requestSite(origin);
|
||||
await settle();
|
||||
bg.connectApproval(pending.id()).decide(true, remember);
|
||||
await settle();
|
||||
expect(pending.result()).toEqual({ result: [signer.address] });
|
||||
}
|
||||
|
||||
// Settings, opened over the background's storage and wired to it the way
|
||||
// the popup is: what Settings sends reaches the background from the
|
||||
// extension's own page, and the answer comes back.
|
||||
async function openSettings(bg) {
|
||||
const lists = {};
|
||||
const element = (id) => (lists[id] ||= fakeSiteList());
|
||||
global.document = { getElementById: element };
|
||||
global.chrome.runtime.sendMessage = (msg, callback) => {
|
||||
const { sendResponse } = bg.send(msg, bg.fromPopup);
|
||||
if (callback) callback(sendResponse.mock.calls[0]?.[0]);
|
||||
};
|
||||
await require("../src/shared/state").loadState();
|
||||
await require("../src/popup/views/settings").renderSiteLists();
|
||||
return {
|
||||
allowed: element("settings-allowed-sites"),
|
||||
connected: element("settings-connected-sites"),
|
||||
// Click the [x] beside a site, and let what it sends run.
|
||||
remove: async (list, hostname) => {
|
||||
expect(listed(list)).toContain(hostname);
|
||||
const button = list.buttons.find(
|
||||
(b) => b.dataset.hostname === hostname,
|
||||
);
|
||||
await button.click();
|
||||
await settle();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
delete global.document;
|
||||
});
|
||||
|
||||
test("Settings lists a site connected without Remember", async () => {
|
||||
const bg = loadBackground({ actionPopup: true });
|
||||
await connect(bg, FRESH_ORIGIN, false);
|
||||
|
||||
const settings = await openSettings(bg);
|
||||
|
||||
expect(listed(settings.connected)).toEqual(["fresh.example"]);
|
||||
expect(listed(settings.allowed)).toEqual([HOSTNAME]);
|
||||
});
|
||||
|
||||
test("removing a site connected without Remember disconnects it and tells its tabs", async () => {
|
||||
const bg = loadBackground({ actionPopup: true });
|
||||
await connect(bg, FRESH_ORIGIN, false);
|
||||
const sentToTabs = [];
|
||||
global.chrome.tabs = {
|
||||
query: (q, cb) =>
|
||||
cb([
|
||||
{ id: 1, url: FRESH_ORIGIN + "/app" },
|
||||
{ id: 2, url: ORIGIN + "/app" },
|
||||
]),
|
||||
sendMessage: (tabId, msg, cb) => {
|
||||
sentToTabs.push({ tabId, msg });
|
||||
cb();
|
||||
},
|
||||
};
|
||||
const settings = await openSettings(bg);
|
||||
|
||||
await settings.remove(settings.connected, "fresh.example");
|
||||
|
||||
expect(await siteAccounts(bg)).toEqual({ result: [] });
|
||||
expect(sentToTabs).toEqual([
|
||||
{
|
||||
tabId: 1,
|
||||
msg: {
|
||||
type: "AUTISTMASK_EVENT",
|
||||
eventName: "accountsChanged",
|
||||
data: [],
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(listed(settings.connected)).toEqual([]);
|
||||
// The other site is untouched.
|
||||
expect(await siteAccounts(bg, ORIGIN)).toEqual({
|
||||
result: [signer.address],
|
||||
});
|
||||
});
|
||||
|
||||
// The same hostname can hold both kinds of connection: one origin allowed
|
||||
// without Remember, then another, on a different port, allowed with it.
|
||||
test("removing a remembered site also ends its connection made without Remember", async () => {
|
||||
const bg = loadBackground({ actionPopup: true });
|
||||
await connect(bg, FRESH_ORIGIN, false);
|
||||
await connect(bg, FRESH_OTHER_PORT, true);
|
||||
const settings = await openSettings(bg);
|
||||
|
||||
await settings.remove(settings.allowed, "fresh.example");
|
||||
|
||||
expect(await siteAccounts(bg, FRESH_OTHER_PORT)).toEqual({
|
||||
result: [],
|
||||
});
|
||||
expect(await siteAccounts(bg, FRESH_ORIGIN)).toEqual({ result: [] });
|
||||
});
|
||||
|
||||
test("a page can neither remove a site nor list the connected ones", async () => {
|
||||
const bg = loadBackground({ actionPopup: true });
|
||||
await connect(bg, FRESH_ORIGIN, false);
|
||||
const page = { url: FRESH_ORIGIN + "/index.html" };
|
||||
|
||||
const remove = bg.send(
|
||||
{ type: "AUTISTMASK_REMOVE_SITE", hostname: "fresh.example" },
|
||||
page,
|
||||
);
|
||||
const list = bg.send({ type: "AUTISTMASK_GET_CONNECTED_SITES" }, page);
|
||||
|
||||
expect(remove.sendResponse).toHaveBeenCalledWith({
|
||||
error: "Unauthorized sender",
|
||||
});
|
||||
expect(list.sendResponse).toHaveBeenCalledWith({
|
||||
error: "Unauthorized sender",
|
||||
});
|
||||
expect(await siteAccounts(bg)).toEqual({ result: [signer.address] });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user