harden: stop the background reading the shared state singleton, and enforce it at build time (closes #324)
Five defects, one of which destroyed every wallet, came from src/background reading and writing the module-level state singleton the MV3 worker never populates, which silently served DEFAULT_STATE. Each point fix created the next defect. The background now has its own per-call getState() and a queued read-modify-write updateState(); the singleton is unreachable from it, and an unpopulated read throws instead of serving defaults. The prohibition is enforced by the build, not by review: build.js asserts over esbuild's own metafile that no forbidden module is an input of a background bundle, so every specifier syntax esbuild resolves is covered, and both halves of the table are checked for rot -- a stale key, a stale module, an empty list, or an unlisted entry point under src/background/ all fail the build. The ESLint rule remains as fast local feedback and reads the same shared table. Known bounds are documented where the table lives. Also closes #320: getProvider() now requires a validated network id, so a cold worker no longer prepares a non-mainnet dApp transaction for mainnet and gets refused by the wallet's own verifier. backgroundRefresh() no longer mutates address objects across a network round trip, the broadcast path takes its endpoint and chain id from one snapshot, and eight test storage stubs now structured-clone on get as the real chrome.storage.local does. closes #320
This commit was merged in pull request #344.
This commit is contained in:
@@ -10,32 +10,12 @@
|
||||
//
|
||||
// Both cases below drive the real state.js module through two independent
|
||||
// module registries sharing one storage backend, the way two real extension
|
||||
// pages share one chrome.storage.local. The storage stub structured-clones
|
||||
// on both get and set — a stub that hands back the object it was given
|
||||
// aliases the caller's own mutation and would make this entire defect class
|
||||
// invisible (see https://git.eeqj.de/sneak/AutistMask/issues/324).
|
||||
// pages share one chrome.storage.local. The shared stub structured-clones on
|
||||
// both get and set — a stub that hands back the object it was given aliases
|
||||
// the caller's own mutation and would make this entire defect class invisible
|
||||
// (see https://git.eeqj.de/sneak/AutistMask/issues/324).
|
||||
|
||||
function makeStorage() {
|
||||
let store = {};
|
||||
return {
|
||||
get: async (keys) => {
|
||||
const wanted =
|
||||
keys === undefined || keys === null
|
||||
? Object.keys(store)
|
||||
: [].concat(keys);
|
||||
const out = {};
|
||||
for (const key of wanted) {
|
||||
if (key in store) out[key] = structuredClone(store[key]);
|
||||
}
|
||||
return out;
|
||||
},
|
||||
set: async (items) => {
|
||||
for (const [key, value] of Object.entries(items)) {
|
||||
store[key] = structuredClone(value);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
const { makeStorageStub } = require("./support/storageStub");
|
||||
|
||||
// One extension page: a fresh module registry over the shared storage.
|
||||
// state.js resolves the storage API at require time, so the stub has to be
|
||||
@@ -43,7 +23,7 @@ function makeStorage() {
|
||||
// singleton, so each page needs its own registry to hold its own copy.
|
||||
function loadPage(storage) {
|
||||
jest.resetModules();
|
||||
globalThis.chrome = { storage: { local: storage } };
|
||||
globalThis.chrome = { storage: { local: storage.local } };
|
||||
return {
|
||||
state: require("../src/shared/state"),
|
||||
helpers: require("../src/popup/views/helpers"),
|
||||
@@ -118,7 +98,7 @@ describe("a save from a page that never saw a wallet another page added", () =>
|
||||
// a save from a second page loaded before that wallet existed. Both
|
||||
// wallets must survive.
|
||||
test("both wallets are in storage afterwards", async () => {
|
||||
const storage = makeStorage();
|
||||
const storage = makeStorageStub();
|
||||
await storage.set({ autistmask: { wallets: [W1] } });
|
||||
|
||||
// Loaded while storage held only Wallet 1, and never reloads —
|
||||
@@ -171,7 +151,7 @@ describe("the approval-window reproduction", () => {
|
||||
test("the wallet added in the popup survives confirming the approval", async () => {
|
||||
globalThis.document = makeDocument();
|
||||
|
||||
const storage = makeStorage();
|
||||
const storage = makeStorageStub();
|
||||
await storage.set({ autistmask: { wallets: [W1] } });
|
||||
|
||||
// The background opens the approval window on the approve-tx
|
||||
@@ -223,7 +203,7 @@ describe("the approval-window reproduction", () => {
|
||||
// membership" collided as the same field.
|
||||
describe("background refresh racing a wallet added on another page", () => {
|
||||
test("the wallet added elsewhere survives background's stale balance save", async () => {
|
||||
const storage = makeStorage();
|
||||
const storage = makeStorageStub();
|
||||
await storage.set({ autistmask: { wallets: [W1] } });
|
||||
|
||||
// "background": loads first, and its save is the one that lands
|
||||
@@ -263,7 +243,7 @@ describe("background refresh racing a wallet added on another page", () => {
|
||||
|
||||
describe("background refresh racing a wallet deleted on another page", () => {
|
||||
test("the wallet deleted elsewhere stays deleted after background's stale balance save", async () => {
|
||||
const storage = makeStorage();
|
||||
const storage = makeStorageStub();
|
||||
await storage.set({ autistmask: { wallets: [W1, W2] } });
|
||||
|
||||
const background = loadPage(storage);
|
||||
@@ -324,7 +304,7 @@ function revokeSite(pageState, hostname) {
|
||||
|
||||
describe("a dApp approval racing a stale Settings page's later save", () => {
|
||||
test("the fresh approval survives Settings revoking an unrelated site", async () => {
|
||||
const storage = makeStorage();
|
||||
const storage = makeStorageStub();
|
||||
await storage.set({
|
||||
autistmask: {
|
||||
wallets: [W1],
|
||||
@@ -362,7 +342,7 @@ describe("a dApp approval racing a stale Settings page's later save", () => {
|
||||
|
||||
describe("a revoked site permission against a stale page's later save", () => {
|
||||
test("the revocation holds even when the stale page approves something else", async () => {
|
||||
const storage = makeStorage();
|
||||
const storage = makeStorageStub();
|
||||
await storage.set({
|
||||
autistmask: {
|
||||
wallets: [W1],
|
||||
@@ -413,7 +393,7 @@ function legacyWallet(name, secret) {
|
||||
|
||||
describe("two wallets independently created with a colliding identity", () => {
|
||||
test("both survive, encryptedSecret included, instead of one silently replacing the other", async () => {
|
||||
const storage = makeStorage();
|
||||
const storage = makeStorageStub();
|
||||
await storage.set({ autistmask: { wallets: [W1] } });
|
||||
|
||||
// Both pages load before either has created their malformed wallet,
|
||||
|
||||
Reference in New Issue
Block a user