Found by the re-review of #360 (#360 (comment)), and a sibling case found during its rework. Both pre-existing on next, both deliberately left out of that PR's scope.
1. allowedSites — a working popup that silently never persists again
A stored allowedSites whose value is not a list — e.g. {"0xabc…": "notalist"} — passes the state gate and renders a working popup, then throws an unhandled TypeError: base.map is not a function at src/shared/state.js:217.
The failure mode is the nastiest shape available: the UI looks entirely healthy, and every subsequent save silently fails. The user keeps using the wallet while nothing they do is persisted. That is worse than the blank popup #311 fixed, because a blank popup is at least visibly broken.
Reproduced independently twice — by the reviewer and again during rework, where allowedSites: {a: 1} killed the jest worker as an unhandled rejection after the popup had rendered.
2. fraudContracts — same class, milder consequence
fraudContracts is assigned as saved.x || default with no type check, and is then structurally dereferenced at src/popup/views/send.js:124: (state.fraudContracts || []).map((a) => a.toLowerCase()).
It does not blank the popup — the boot path reaches it only via loadHomeTxs(), which catches and renders "Failed to load transactions", and the send view is not on boot. So the consequence is a broken send screen rather than silent data loss, but the shape is identical: a container floored (or not) while its entries are dereferenced unchecked.
Why these are together
#360 established the idiom for this class — floor the container and its entries, dropping anything that cannot be safely dereferenced — and applied it to trackedTokens and tokenBalances. These are the two fields it did not reach. Fix them with that same idiom rather than inventing a third one.
In milestone 1.0.0 on the strength of case 1: silent non-persistence in a wallet is a data-loss defect, and the 1.0 standard is being able to actually use this in low-volume production.
Definition of done
Malformed allowedSites, deniedSites and fraudContracts entries cannot reach a .map() or a .toLowerCase(). Floor the per-entry shape, not just the container, following the idiom settled in #360.
A save that fails must not fail silently — the user is told, or the state is repaired, but the wallet must never continue running against storage that is quietly rejecting every write.
Test per field: a malformed entry yields a usable popup whose writes actually persist, and a usable send screen. Each fails against current head — state the mutation and observed result.
make check green.
Found by the re-review of https://git.eeqj.de/sneak/AutistMask/pulls/360 (https://git.eeqj.de/sneak/AutistMask/pulls/360#issuecomment-69234), and a sibling case found during its rework. Both pre-existing on `next`, both deliberately left out of that PR's scope.
## 1. `allowedSites` — a working popup that silently never persists again
A stored `allowedSites` whose value is not a list — e.g. `{"0xabc…": "notalist"}` — passes the state gate and renders a **working** popup, then throws an unhandled `TypeError: base.map is not a function` at `src/shared/state.js:217`.
The failure mode is the nastiest shape available: the UI looks entirely healthy, and **every subsequent save silently fails**. The user keeps using the wallet while nothing they do is persisted. That is worse than the blank popup https://git.eeqj.de/sneak/AutistMask/issues/311 fixed, because a blank popup is at least visibly broken.
Reproduced independently twice — by the reviewer and again during rework, where `allowedSites: {a: 1}` killed the jest worker as an unhandled rejection *after* the popup had rendered.
## 2. `fraudContracts` — same class, milder consequence
`fraudContracts` is assigned as `saved.x || default` with no type check, and is then structurally dereferenced at `src/popup/views/send.js:124`: `(state.fraudContracts || []).map((a) => a.toLowerCase())`.
It does not blank the popup — the boot path reaches it only via `loadHomeTxs()`, which catches and renders "Failed to load transactions", and the send view is not on boot. So the consequence is a broken send screen rather than silent data loss, but the shape is identical: a container floored (or not) while its entries are dereferenced unchecked.
## Why these are together
https://git.eeqj.de/sneak/AutistMask/pulls/360 established the idiom for this class — floor the container **and** its entries, dropping anything that cannot be safely dereferenced — and applied it to `trackedTokens` and `tokenBalances`. These are the two fields it did not reach. Fix them with that same idiom rather than inventing a third one.
In milestone 1.0.0 on the strength of case 1: silent non-persistence in a wallet is a data-loss defect, and the 1.0 standard is being able to actually use this in low-volume production.
## Definition of done
- [ ] Malformed `allowedSites`, `deniedSites` and `fraudContracts` entries cannot reach a `.map()` or a `.toLowerCase()`. Floor the per-entry shape, not just the container, following the idiom settled in https://git.eeqj.de/sneak/AutistMask/pulls/360.
- [ ] A save that fails must not fail silently — the user is told, or the state is repaired, but the wallet must never continue running against storage that is quietly rejecting every write.
- [ ] Test per field: a malformed entry yields a usable popup whose writes actually persist, and a usable send screen. Each fails against current head — state the mutation and observed result.
- [ ] `make check` green.
clawbot
added this to the 1.0.0 milestone 2026-08-23 19:02:23 +02:00
clawbot
changed title from fix: a malformed allowedSites entry gives a working popup that silently never persists again to fix: malformed allowedSites and fraudContracts entries are dereferenced without a floor2026-08-23 19:21:22 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found by the re-review of #360 (#360 (comment)), and a sibling case found during its rework. Both pre-existing on
next, both deliberately left out of that PR's scope.1.
allowedSites— a working popup that silently never persists againA stored
allowedSiteswhose value is not a list — e.g.{"0xabc…": "notalist"}— passes the state gate and renders a working popup, then throws an unhandledTypeError: base.map is not a functionatsrc/shared/state.js:217.The failure mode is the nastiest shape available: the UI looks entirely healthy, and every subsequent save silently fails. The user keeps using the wallet while nothing they do is persisted. That is worse than the blank popup #311 fixed, because a blank popup is at least visibly broken.
Reproduced independently twice — by the reviewer and again during rework, where
allowedSites: {a: 1}killed the jest worker as an unhandled rejection after the popup had rendered.2.
fraudContracts— same class, milder consequencefraudContractsis assigned assaved.x || defaultwith no type check, and is then structurally dereferenced atsrc/popup/views/send.js:124:(state.fraudContracts || []).map((a) => a.toLowerCase()).It does not blank the popup — the boot path reaches it only via
loadHomeTxs(), which catches and renders "Failed to load transactions", and the send view is not on boot. So the consequence is a broken send screen rather than silent data loss, but the shape is identical: a container floored (or not) while its entries are dereferenced unchecked.Why these are together
#360 established the idiom for this class — floor the container and its entries, dropping anything that cannot be safely dereferenced — and applied it to
trackedTokensandtokenBalances. These are the two fields it did not reach. Fix them with that same idiom rather than inventing a third one.In milestone 1.0.0 on the strength of case 1: silent non-persistence in a wallet is a data-loss defect, and the 1.0 standard is being able to actually use this in low-volume production.
Definition of done
allowedSites,deniedSitesandfraudContractsentries cannot reach a.map()or a.toLowerCase(). Floor the per-entry shape, not just the container, following the idiom settled in #360.make checkgreen.fix: a malformed allowedSites entry gives a working popup that silently never persists againto fix: malformed allowedSites and fraudContracts entries are dereferenced without a floor