fix: floor malformed allowedSites, fraudContracts and selectedToken entries (closes #362)
A stored allowedSites whose value was not a list rendered a working popup and then made every subsequent save fail silently, so the user operated a wallet that persisted nothing -- worse than a blank popup, which is at least visibly broken. fraudContracts and selectedToken had the same shape: a container floored by truthiness or not at all, while its entries were dereferenced. Entries are now floored as well as containers, following the idiom #311 established, and a failed save raises a persistent banner instead of vanishing into a swallowed rejection. The per-field justifications that used to live in a hand-written header are replaced by a contract test that drives each field's hostile and falsy values through a real popup boot, so a claim about a field answers to the code rather than to prose. Its guarantee is stated narrowly and deliberately: no structural dereference on the code paths a wholly-corrupted profile takes, which is not every path a stored record takes. The paths it does not drive are named where the claim is made, and are tracked in #379.
This commit was merged in pull request #366.
This commit is contained in:
+78
-3
@@ -53,12 +53,17 @@ function resetRenderedViews() {
|
||||
const ALWAYS_RENDER_ON_BACK = new Set(["main"]);
|
||||
|
||||
// Views that render an address the user picked and cannot be rendered
|
||||
// without one.
|
||||
// without one. "confirm-tx" is here because its Sign button dereferences
|
||||
// `state.wallets[state.selectedWallet].encryptedSecret`
|
||||
// (src/popup/views/confirmTx.js) behind no guard of its own — a screen that
|
||||
// can only throw when the user presses its one button must not be restored
|
||||
// onto.
|
||||
const ADDRESS_VIEWS = new Set([
|
||||
"address",
|
||||
"address-token",
|
||||
"receive",
|
||||
"transaction",
|
||||
"confirm-tx",
|
||||
]);
|
||||
|
||||
function needsAddress(view) {
|
||||
@@ -74,6 +79,73 @@ function hasValidAddress(state) {
|
||||
);
|
||||
}
|
||||
|
||||
// The stored viewData ENTRIES each branch below dereferences, as opposed to
|
||||
// the one field it gates on.
|
||||
//
|
||||
// A gate on a single truthy field checks the container, not the entries, and
|
||||
// the dereference is one level below it: a stored `{"currentView":
|
||||
// "success-tx","viewData":{"hash":"0x1"}}` passes `data.hash` and then throws
|
||||
// on `address.toLowerCase()` inside addressTitle() (src/popup/views/
|
||||
// helpers.js), out of restoreView(), which src/popup/index.js does not guard —
|
||||
// so the rest of popup init never runs. txStatus.restoreWait() has checked its
|
||||
// own branch's fields since it was written; these are the other four.
|
||||
//
|
||||
// Only what actually throws is required. Fields that are compared,
|
||||
// concatenated or escaped coerce (escapeHtml() and displaySymbol() both
|
||||
// String() their argument), so requiring them would refuse a restorable screen
|
||||
// over a cosmetic value.
|
||||
function isText(value) {
|
||||
return typeof value === "string";
|
||||
}
|
||||
|
||||
function isRecord(value) {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
// An address handed to renderAddressHtml()/addressTitle(): both reach
|
||||
// `address.slice()` and `address.toLowerCase()` with no guard.
|
||||
function isAddressText(value) {
|
||||
return isText(value);
|
||||
}
|
||||
|
||||
// Decoded calldata, as decodedDetailsHtml() (src/popup/views/txStatus.js)
|
||||
// walks it: `for (const d of decoded.details)` needs an iterable, and each
|
||||
// entry's `address` reaches toAddressHtml(). Absent or falsy is the ordinary
|
||||
// case and short-circuits before either.
|
||||
function isRenderableDecoded(value) {
|
||||
if (!value) return true;
|
||||
if (!isRecord(value)) return false;
|
||||
if (!value.details) return true;
|
||||
if (!Array.isArray(value.details)) return false;
|
||||
return value.details.every(
|
||||
(entry) =>
|
||||
isRecord(entry) && (!entry.address || isAddressText(entry.address)),
|
||||
);
|
||||
}
|
||||
|
||||
// The pending transaction confirmTx.show() renders: `token` reaches
|
||||
// renderAddressHtml() when it is not "ETH", and `from`/`to` reach
|
||||
// addressTitle(), makeBlockie() and getLocalWarnings().
|
||||
function isRenderablePendingTx(value) {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
isText(value.token) &&
|
||||
isAddressText(value.from) &&
|
||||
isAddressText(value.to)
|
||||
);
|
||||
}
|
||||
|
||||
// The stored transaction transactionDetail.render() shows. contractAddress is
|
||||
// optional on an ETH transfer, and reaches addressDotHtml() when it is there.
|
||||
function isRenderableTx(value) {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
isAddressText(value.from) &&
|
||||
isAddressText(value.to) &&
|
||||
(!value.contractAddress || isAddressText(value.contractAddress))
|
||||
);
|
||||
}
|
||||
|
||||
// Render `view` from persisted state. Each view module shows itself, so a
|
||||
// true return means the view is both rendered and on screen.
|
||||
//
|
||||
@@ -107,11 +179,11 @@ function renderView(view, state, views) {
|
||||
views.settingsAddToken.show();
|
||||
return true;
|
||||
case "confirm-tx":
|
||||
if (!data.pendingTx) return false;
|
||||
if (!isRenderablePendingTx(data.pendingTx)) return false;
|
||||
views.confirmTx.restore();
|
||||
return true;
|
||||
case "transaction":
|
||||
if (!data.tx) return false;
|
||||
if (!isRenderableTx(data.tx)) return false;
|
||||
views.transactionDetail.render();
|
||||
return true;
|
||||
case "wait-tx":
|
||||
@@ -120,10 +192,13 @@ function renderView(view, state, views) {
|
||||
return Boolean(views.txStatus.restoreWait());
|
||||
case "success-tx":
|
||||
if (!data.hash) return false;
|
||||
if (!isAddressText(data.to)) return false;
|
||||
if (!isRenderableDecoded(data.decoded)) return false;
|
||||
views.txStatus.renderSuccess();
|
||||
return true;
|
||||
case "error-tx":
|
||||
if (!data.message) return false;
|
||||
if (!isAddressText(data.to)) return false;
|
||||
views.txStatus.renderError();
|
||||
return true;
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user