harden: key remembered site permissions by full origin (closes #402)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 3s
e2e / e2e-firefox (push) Failing after 2s

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 is contained in:
2026-10-04 15:43:58 +00:00
parent f24b5bca19
commit e557f21bb0
29 changed files with 470 additions and 231 deletions
+23 -12
View File
@@ -59,24 +59,32 @@ describe("the floor under allowedSites and deniedSites", () => {
}
});
test(`an ${field} entry whose value is not a hostname list is dropped`, () => {
for (const bad of ["dapp.example", 42, null, { a: 1 }, true]) {
test(`an ${field} entry whose value is not an origin list is dropped`, () => {
for (const bad of [
"https://dapp.example",
42,
null,
{ a: 1 },
true,
]) {
expect(
normalizePersisted({ [field]: { [ADDRESS]: bad } })[field],
).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(
normalizePersisted({
[field]: { [ADDRESS]: [42, null, "dapp.example", {}] },
[field]: {
[ADDRESS]: [42, null, "https://dapp.example", {}],
},
})[field],
).toEqual({ [ADDRESS]: ["dapp.example"] });
).toEqual({ [ADDRESS]: ["https://dapp.example"] });
});
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);
@@ -87,10 +95,13 @@ describe("the floor under allowedSites and deniedSites", () => {
test(`a good ${field} entry beside a malformed one survives`, () => {
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`, () => {
@@ -100,7 +111,7 @@ describe("the floor under allowedSites and deniedSites", () => {
// saveState()'s merge hands to the prototype setter on the next
// write.
const saved = JSON.parse(
'{"' + field + '":{"__proto__":["evil.invalid"]}}',
'{"' + field + '":{"__proto__":["https://evil.invalid"]}}',
);
const out = normalizePersisted(saved);
@@ -197,7 +208,7 @@ describe("a malformed allowedSites entry", () => {
const MALFORMED = [
{ name: "a string", value: "notalist" },
{ 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) {
@@ -231,7 +242,7 @@ describe("a malformed allowedSites entry", () => {
const env = await bootPopup(
unversionedValidProfile({
allowedSites: {
[ADDRESS]: ["dapp.example"],
[ADDRESS]: ["https://dapp.example"],
[TOKEN_ADDRESS]: "notalist",
},
}),
@@ -239,7 +250,7 @@ describe("a malformed allowedSites entry", () => {
expect(env.pageErrors).toEqual([]);
expect(env.storage.read("autistmask").allowedSites).toEqual({
[ADDRESS]: ["dapp.example"],
[ADDRESS]: ["https://dapp.example"],
});
});