pre-1.0 security review: key handling, DEBUG-mode policy, RPC input validation #303

Closed
opened 2026-08-17 10:11:24 +02:00 by clawbot · 2 comments
Collaborator

The repo's stated Next Step, and the last thing standing between milestone 1.0.0 and a tag. Milestone 1.0.0 is complete (61 closed, 0 open) and #190 is merge-ready, but no security review of the extension as a whole has been done — only the individual defects that were filed and fixed one at a time.

This is an audit task, not a code-change unit. Its deliverable is findings. Every finding gets its own issue with its own definition of done; this issue is closed when the audit is complete and its findings are filed, not when they are fixed.

Why it is worth doing as a whole rather than as its parts

Every unit that landed in 1.0.0 was reviewed against its own definition of done. Nothing has looked at the assembled system for defects that live between units — a guard that is correct in isolation but reachable around, an invariant two units each half-enforce, a path that only exists once several changes coexist. The approval machinery alone was reworked by #271, #275 and #280 in the same session.

Scope

Three lanes, as named in TODO.md:

  1. Key and secret handling — key material at rest and in memory; encrypt/decrypt paths; what crosses the extension messaging boundary; what reaches the DOM and when it is cleared; what survives a view exit, a popup close, or a worker restart; what a compromised content script or page can reach.
  2. DEBUG-mode policy and build integrity — that DEBUG cannot be on in a shipped bundle, that verify-build cannot be satisfied by a bundle that did not come from this tree, and that no debug-only path leaks secrets or bypasses a gate when enabled.
  3. RPC and page input validation — every value arriving from a dApp, a page, an RPC response or the blocklist artifact. Type confusion, missing bounds, values trusted because a sibling field was checked, and anything where a malformed input reaches signing or display.

Rules for the audit

  • Adversarial, not confirmatory. The output that matters is what is wrong. Do not restate what is right beyond what is needed to show the search was real.
  • Every finding must be reproduced, not theorised — a failing test, a probe, or a concrete input-to-outcome path. A finding that cannot be demonstrated is filed as a question, labelled as such.
  • Severity is stated in terms of consequence, not adjectives: what an attacker gets, what the user loses, and what has to be true first.
  • Do not fix anything. Do not open a PR. Findings only.

Definition of done

  • All three lanes audited, with the method and coverage stated — including what was NOT examined and why.
  • Each finding filed as its own issue, with reproduction, consequence, preconditions, and an acceptable fix.
  • Findings that turn out to be non-issues are recorded as such rather than silently dropped, so the same ground is not re-covered.
  • A summary comment on this issue listing every filed finding by number, and an explicit statement of whether anything found should block the 1.0.0 tag.
  • No code change in this unit; make check untouched and still green.
The repo's stated Next Step, and the last thing standing between milestone 1.0.0 and a tag. Milestone 1.0.0 is complete (61 closed, 0 open) and https://git.eeqj.de/sneak/AutistMask/pulls/190 is merge-ready, but no security review of the extension as a whole has been done — only the individual defects that were filed and fixed one at a time. This is an **audit task, not a code-change unit.** Its deliverable is findings. Every finding gets its own issue with its own definition of done; this issue is closed when the audit is complete and its findings are filed, not when they are fixed. ## Why it is worth doing as a whole rather than as its parts Every unit that landed in 1.0.0 was reviewed against its own definition of done. Nothing has looked at the assembled system for defects that live *between* units — a guard that is correct in isolation but reachable around, an invariant two units each half-enforce, a path that only exists once several changes coexist. The approval machinery alone was reworked by https://git.eeqj.de/sneak/AutistMask/issues/271, https://git.eeqj.de/sneak/AutistMask/issues/275 and https://git.eeqj.de/sneak/AutistMask/issues/280 in the same session. ## Scope Three lanes, as named in `TODO.md`: 1. **Key and secret handling** — key material at rest and in memory; encrypt/decrypt paths; what crosses the extension messaging boundary; what reaches the DOM and when it is cleared; what survives a view exit, a popup close, or a worker restart; what a compromised content script or page can reach. 2. **DEBUG-mode policy and build integrity** — that `DEBUG` cannot be on in a shipped bundle, that `verify-build` cannot be satisfied by a bundle that did not come from this tree, and that no debug-only path leaks secrets or bypasses a gate when enabled. 3. **RPC and page input validation** — every value arriving from a dApp, a page, an RPC response or the blocklist artifact. Type confusion, missing bounds, values trusted because a sibling field was checked, and anything where a malformed input reaches signing or display. ## Rules for the audit - **Adversarial, not confirmatory.** The output that matters is what is wrong. Do not restate what is right beyond what is needed to show the search was real. - **Every finding must be reproduced**, not theorised — a failing test, a probe, or a concrete input-to-outcome path. A finding that cannot be demonstrated is filed as a question, labelled as such. - **Severity is stated in terms of consequence**, not adjectives: what an attacker gets, what the user loses, and what has to be true first. - Do not fix anything. Do not open a PR. Findings only. ## Definition of done - [ ] All three lanes audited, with the method and coverage stated — including what was NOT examined and why. - [ ] Each finding filed as its own issue, with reproduction, consequence, preconditions, and an acceptable fix. - [ ] Findings that turn out to be non-issues are recorded as such rather than silently dropped, so the same ground is not re-covered. - [ ] A summary comment on this issue listing every filed finding by number, and an explicit statement of whether anything found should block the 1.0.0 tag. - [ ] No code change in this unit; `make check` untouched and still green.
clawbot added this to the 1.0.0 milestone 2026-08-20 11:26:42 +02:00
Author
Collaborator

Audit verdict: NO. Not deployable, do not tag 1.0.0.

Audited next @ ff3387d in a separate clone. Baseline confirmed green first: make check exit 0, make build 4 bundles DEBUG-off, make test-e2e 52/52, make test-e2e-firefox 8/8. All browser work ran in the pinned containers, driving the real popup, the real background worker and real dApp round trips via throwaway probes built on tests/e2e/harness.js and network.js. Findings are reproduced, not read.

Two of the blockers are silent, funds-destroying defects reachable in ordinary use with no attacker involved: a real dApp approval window silently deletes a wallet the user added while it was open (encrypted secret gone, no error, dApp transaction succeeds normally), and the wallet's own ERC-20 send signs a transfer amount it never displayed — the screen said 0.25 E2E, the signed artifact carried 250000000000000000. There is also no release, no artifact, no packaging and no signing: on release Firefox this cannot be permanently installed at all.

The signing and verification core is genuinely strong — the type allowlist, the canonical-bytes rebuild, from pinning, and the password never leaving the popup all hold under probing. The failures are in the surfaces around it.

Blockers (all in milestone 1.0.0)

#304 A second extension page silently deletes a wallet — saveState() is a last-writer-wins full-blob overwrite
#305 The popup's own ERC-20 send signs an amount it never displayed
#306 The dApp approval screen renders a 5,000-token transfer as Amount 0.0000
#307 A hostile ERC-20 symbol renders as live HTML in the popup — cross-origin iframe over the wallet UI
#308 Any web page can switch the network and destroy the user's custom RPC endpoint, unprompted
#309 make build can produce an INSECURE debug bundle and verify-build certifies it green
#310 No packaging, no artifact, no signing — cannot be installed durably on either browser
#311 Stored state has no version and no migration; a corrupt blob bricks the popup and every dApp call
#312 A forgotten password permanently wedges the wallet

#190 is held WIP: until these close.

Non-blocker findings

Recorded here in full so none is lost; each will be filed as its own issue in the next round. None gates the tag on its own.

  • The session that creates the first wallet never refreshes balances again — src/popup/index.js:203-209 starts doRefreshAndRender() and its 10s interval only when state.hasWallet was already true at load. Observed ETH0.0000 for 24s after creation, then correct on reopen. A funded new user sees 0.0000 and Send refuses.
  • A dApp promise can hang forever: kill the MV3 worker mid-approval, then confirm. The approval window correctly refuses and nothing broadcasts (verified 0 broadcasts), but eth_sendTransaction on the page was still {"settled":"pending"} after 30s. src/content/index.js:69-74 swallows the sendMessage rejection; src/content/inpage.js:88-97 has no timeout. Same family as #262 and contradicts the #280 claim in PR 190's body.
  • eth_signTypedData_v4 never checks domain.chainId against the active network — src/background/index.js:801-834, src/shared/approvalVerify.js:563-593. A site connected on Sepolia can obtain a valid mainnet Permit2 signature. The chain id is on screen as one unlabelled domain line, with no contradiction flagged.
  • The Uniswap decoder is applied to any to address — src/popup/views/approval.js:143, src/shared/uniswap.js:431-435. A hostile contract exposing execute(bytes,bytes[],uint256) gets its calldata rendered as "Swap X to Y / Protocol: Uniswap Universal Router", with its own address under "Protocol".
  • Page-supplied gasLimit and nonce are accepted — src/shared/approvalTx.js:54-66. Supplying gasLimit disables estimation, so an always-reverting transaction displays identically to a good one. Compat bug in the same place: dApps send gas, not gasLimit, so a well-behaved dApp's limit is dropped while a hostile one's is honoured.
  • #approve-tx-data has no max-height (src/popup/index.html:1526-1529) unlike #approve-sign-message (:1600-1604); a few hundred KB of calldata pushes the fee, nonce and Confirm button off-screen.
  • Fee ceilings do not bound the loss: MAX_GAS_LIMIT * MAX_FEE_PER_GAS = 10,000 ETH (src/shared/approvalVerify.js:134-139). A hostile RPC's getFeeData() passes the ceiling and is then "verified as matching what was displayed".
  • Phishing blocklist trailing-dot evasion: src/shared/domainHash.js:29-35 lowercases only, new URL().hostname preserves a trailing dot, and hostnameVariants (src/shared/phishingDomains.js:116-125) never yields the dotless form. https://evil.com./ resolves normally and the red banner stays hidden. Uppercase and sub-subdomain evasions are correctly closed.
  • eth_sendRawTransaction and 20 read methods are proxied to every origin with no connection check — src/background/index.js:637,841-848. Any page can use the wallet as an open relay and, if state.rpcUrl carries an Alchemy/Infura key, consume it.
  • AUTISTMASK_ACTIVE_CHANGED is excluded from POPUP_ONLY_TYPES (src/background/index.js:1168-1176, handler at :1503) and rejects every pending approval. Not page-reachable today, but it is the one privileged handler outside the guard.
  • log.infof/warnf/errorf are always on in a release build — src/shared/log.js:24. All 53 call sites checked; none passes a secret today. Latent hazard.
  • The runtime debugMode toggle enables debugFetch (src/shared/log.js:46-53), which logs the user's addresses and RPC endpoint to the popup console. No secret, no gate bypassed. It is popup-only by accident (setRuntimeDebug is never called in the background) — worth a comment so nobody "fixes" it.
  • Secrets go to the system clipboard with no auto-clear — src/popup/views/showPhrase.js:148, exportPrivkey.js:168. The views wipe the DOM meticulously, then hand the same secret to a clipboard with history.
  • The debug test mnemonic ships in all four release bundles and the branch survives as a live ternary (DEBUG crosses a CommonJS boundary so esbuild cannot fold it). Verified unreachable — DEBUG:!1 in every release bundle — but the guarantee rests on a module export's value, not on removed code.
  • Restore loses everything except key material. Real wipe-and-reimport: {"walletName":"Cold storage","addressCount":3,"trackedTokens":["E2E"],"allowedSites":{...},"utcTimestamps":true,"theme":"dark","rpcUrl":"http://127.0.0.1:8545"} became {"walletName":"Wallet 1","addressCount":1,"trackedTokens":[],"allowedSites":{},"utcTimestamps":false,"theme":"system","rpcUrl":"https://ethereum-rpc.publicnode.com"}. First address identical; addresses 2 and 3 re-derivable with "+"; everything else silently gone and the custom RPC reverted to a public node.
  • No sourcemaps are emitted and bundles are minified (build.js:149). dist/constants-bundles.txt and dist/styles.css sit at dist/ root, outside both browser dirs.
  • ENS names are cached in popup localStorage (src/shared/ens.js:18,32), outside the state blob and outside every clearing path. Privacy only.
  • window.postMessage responses use "*" at src/content/index.js:66,86 where location.origin is used correctly at :44. Same window, so no cross-origin delivery; defence in depth.
  • Unicode direction overrides are not neutralised in displayed personal_sign messages (src/popup/views/approval.js:409,411).
  • accessList is accepted, signed and verified but never displayed (src/shared/approvalTx.js:62, approvalVerify.js:308-313 vs approval.js:166-194) — the one attacker-controllable signed field that is invisible. Impact bounded to gas, which is displayed.
  • personal_sign rejects plain-UTF-8 payloads that other wallets accept (decodeHexMessage to getBytes throws).
  • The Chrome e2e suite never clicks #btn-confirm-send: the popup's own Send to ConfirmTx to Sign to WaitTx path has no end-to-end coverage at all. That is how #305 survived.

Checked and genuinely fine — do not re-cover

  • No lock screen is correct here. The password is required per signing operation, so there is no decrypted material to protect. Verified chrome.storage.session and chrome.storage.sync are both empty; only storage.local is used, and no storage.sync reference exists in src/.
  • Nothing plaintext is ever written to storage. Wallets are pushed only after encryptWithPassword returns. Argon2id + XSalsa20-Poly1305 via libsodium, WASM backend enforced and loudly reported if lost.
  • The password never crosses the extension messaging boundary. Decryption happens in the popup; only the signed artifact travels. The standing floor from #157 holds.
  • Secret wiping is the strongest part of the code. revealGeneration discards a decrypt landing after a view exit; no decrypted secret is held in any module-level variable; show-phrase and export-privkey are excluded from RESTORABLE_VIEWS.
  • Native ETH send works end to end, artifact verified at eth_sendRawTransaction. WaitTx really is persisted across a popup close.
  • eth_accounts from an unconnected origin correctly returns [].
  • The transaction type allowlist and canonical-bytes rebuild are excellentauthorizationList (EIP-7702) stripped, forbidden, type-excluded and caught by the rebuild.
  • from ownership is pinned at approval time and re-checked before signing. chainId is verified against local state, never against anything the node said, so a lying eth_chainId cannot move the wallet.
  • Content/page boundary: event.source === window checked in all three listeners, neither content script declares all_frames, origin re-derived from sender, no externally_connectable.
  • Blocklist artifact validation fails loud, and txValidation.js/holders.js fail closed — unknown fee is FEE_UNAVAILABLE never zero; unknown holder count is null, never "low".
  • generateMnemonic cannot be reached by the runtime debug toggle, confirmed at bundle level.

Not examined, and why

Vendored blocklist upstream content and pinning (build-time supply chain, hash-pinned by script/vendor-blocklist); the ethers and libsodium dependencies themselves; Firefox MV2 background timer semantics beyond the shared alarm path; unit-suite quality; performance and CSS.

Unverified

  • Everything against a real network with real funds. The RPC is stubbed in both harnesses. Nonce handling against a live mempool, real gas markets, reverts, reorgs, and fee-ceiling behaviour under real getFeeData() are unproven. This is the human pass PR 190 already names; the probes do not replace it.
  • Chrome extension-ID stability across a checkout move — absence of key verified directly, the storage-partition consequence inferred from documented Chrome behaviour. Covered by a DoD item on #310.
  • Firefox storage survival across an add-on remove/re-addgecko.id is stable, which is the right precondition, but no uninstall/reinstall cycle was driven. Given #310 this is the single most important unverified claim.
  • The exact mechanism of the killed-worker pending promise — the hang is demonstrated; whether it is the swallowed rejection or the missing timeout is not isolated. Both need fixing.
  • Whether the ungated AUTISTMASK_ACTIVE_CHANGED handler is reachable from a compromised renderer — no page-reachable path found; a compromised content script was not simulated.
## Audit verdict: NO. Not deployable, do not tag 1.0.0. Audited `next` @ `ff3387d` in a separate clone. Baseline confirmed green first: `make check` exit 0, `make build` 4 bundles DEBUG-off, `make test-e2e` 52/52, `make test-e2e-firefox` 8/8. All browser work ran in the pinned containers, driving the real popup, the real background worker and real dApp round trips via throwaway probes built on `tests/e2e/harness.js` and `network.js`. Findings are reproduced, not read. Two of the blockers are silent, funds-destroying defects reachable in ordinary use **with no attacker involved**: a real dApp approval window silently deletes a wallet the user added while it was open (encrypted secret gone, no error, dApp transaction succeeds normally), and the wallet's own ERC-20 send signs a transfer amount it never displayed — the screen said `0.25 E2E`, the signed artifact carried `250000000000000000`. There is also no release, no artifact, no packaging and no signing: on release Firefox this cannot be permanently installed at all. The signing and verification core is genuinely strong — the type allowlist, the canonical-bytes rebuild, `from` pinning, and the password never leaving the popup all hold under probing. The failures are in the surfaces around it. ### Blockers (all in milestone 1.0.0) | | | |---|---| | https://git.eeqj.de/sneak/AutistMask/issues/304 | A second extension page silently deletes a wallet — `saveState()` is a last-writer-wins full-blob overwrite | | https://git.eeqj.de/sneak/AutistMask/issues/305 | The popup's own ERC-20 send signs an amount it never displayed | | https://git.eeqj.de/sneak/AutistMask/issues/306 | The dApp approval screen renders a 5,000-token transfer as `Amount 0.0000` | | https://git.eeqj.de/sneak/AutistMask/issues/307 | A hostile ERC-20 symbol renders as live HTML in the popup — cross-origin iframe over the wallet UI | | https://git.eeqj.de/sneak/AutistMask/issues/308 | Any web page can switch the network and destroy the user's custom RPC endpoint, unprompted | | https://git.eeqj.de/sneak/AutistMask/issues/309 | `make build` can produce an INSECURE debug bundle and `verify-build` certifies it green | | https://git.eeqj.de/sneak/AutistMask/issues/310 | No packaging, no artifact, no signing — cannot be installed durably on either browser | | https://git.eeqj.de/sneak/AutistMask/issues/311 | Stored state has no version and no migration; a corrupt blob bricks the popup and every dApp call | | https://git.eeqj.de/sneak/AutistMask/issues/312 | A forgotten password permanently wedges the wallet | https://git.eeqj.de/sneak/AutistMask/pulls/190 is held `WIP:` until these close. ### Non-blocker findings Recorded here in full so none is lost; each will be filed as its own issue in the next round. None gates the tag on its own. - The session that creates the **first** wallet never refreshes balances again — `src/popup/index.js:203-209` starts `doRefreshAndRender()` and its 10s interval only when `state.hasWallet` was already true at load. Observed `ETH0.0000` for 24s after creation, then correct on reopen. A funded new user sees `0.0000` and Send refuses. - A dApp promise can hang forever: kill the MV3 worker mid-approval, then confirm. The approval window correctly refuses and nothing broadcasts (verified 0 broadcasts), but `eth_sendTransaction` on the page was still `{"settled":"pending"}` after 30s. `src/content/index.js:69-74` swallows the `sendMessage` rejection; `src/content/inpage.js:88-97` has no timeout. Same family as https://git.eeqj.de/sneak/AutistMask/issues/262 and contradicts the https://git.eeqj.de/sneak/AutistMask/issues/280 claim in PR 190's body. - `eth_signTypedData_v4` never checks `domain.chainId` against the active network — `src/background/index.js:801-834`, `src/shared/approvalVerify.js:563-593`. A site connected on Sepolia can obtain a valid mainnet Permit2 signature. The chain id is on screen as one unlabelled domain line, with no contradiction flagged. - The Uniswap decoder is applied to **any** `to` address — `src/popup/views/approval.js:143`, `src/shared/uniswap.js:431-435`. A hostile contract exposing `execute(bytes,bytes[],uint256)` gets its calldata rendered as "Swap X to Y / Protocol: Uniswap Universal Router", with its own address under "Protocol". - Page-supplied `gasLimit` and `nonce` are accepted — `src/shared/approvalTx.js:54-66`. Supplying `gasLimit` disables estimation, so an always-reverting transaction displays identically to a good one. Compat bug in the same place: dApps send `gas`, not `gasLimit`, so a well-behaved dApp's limit is dropped while a hostile one's is honoured. - `#approve-tx-data` has no `max-height` (`src/popup/index.html:1526-1529`) unlike `#approve-sign-message` (`:1600-1604`); a few hundred KB of calldata pushes the fee, nonce and Confirm button off-screen. - Fee ceilings do not bound the loss: `MAX_GAS_LIMIT * MAX_FEE_PER_GAS` = 10,000 ETH (`src/shared/approvalVerify.js:134-139`). A hostile RPC's `getFeeData()` passes the ceiling and is then "verified as matching what was displayed". - Phishing blocklist trailing-dot evasion: `src/shared/domainHash.js:29-35` lowercases only, `new URL().hostname` preserves a trailing dot, and `hostnameVariants` (`src/shared/phishingDomains.js:116-125`) never yields the dotless form. `https://evil.com./` resolves normally and the red banner stays hidden. Uppercase and sub-subdomain evasions are correctly closed. - `eth_sendRawTransaction` and 20 read methods are proxied to every origin with no connection check — `src/background/index.js:637,841-848`. Any page can use the wallet as an open relay and, if `state.rpcUrl` carries an Alchemy/Infura key, consume it. - `AUTISTMASK_ACTIVE_CHANGED` is excluded from `POPUP_ONLY_TYPES` (`src/background/index.js:1168-1176`, handler at `:1503`) and rejects every pending approval. Not page-reachable today, but it is the one privileged handler outside the guard. - `log.infof/warnf/errorf` are always on in a release build — `src/shared/log.js:24`. All 53 call sites checked; none passes a secret today. Latent hazard. - The runtime `debugMode` toggle enables `debugFetch` (`src/shared/log.js:46-53`), which logs the user's addresses and RPC endpoint to the popup console. No secret, no gate bypassed. It is popup-only by accident (`setRuntimeDebug` is never called in the background) — worth a comment so nobody "fixes" it. - Secrets go to the system clipboard with no auto-clear — `src/popup/views/showPhrase.js:148`, `exportPrivkey.js:168`. The views wipe the DOM meticulously, then hand the same secret to a clipboard with history. - The debug test mnemonic ships in all four release bundles and the branch survives as a live ternary (`DEBUG` crosses a CommonJS boundary so esbuild cannot fold it). Verified unreachable — `DEBUG:!1` in every release bundle — but the guarantee rests on a module export's value, not on removed code. - Restore loses everything except key material. Real wipe-and-reimport: `{"walletName":"Cold storage","addressCount":3,"trackedTokens":["E2E"],"allowedSites":{...},"utcTimestamps":true,"theme":"dark","rpcUrl":"http://127.0.0.1:8545"}` became `{"walletName":"Wallet 1","addressCount":1,"trackedTokens":[],"allowedSites":{},"utcTimestamps":false,"theme":"system","rpcUrl":"https://ethereum-rpc.publicnode.com"}`. First address identical; addresses 2 and 3 re-derivable with "+"; everything else silently gone and the custom RPC reverted to a public node. - No sourcemaps are emitted and bundles are minified (`build.js:149`). `dist/constants-bundles.txt` and `dist/styles.css` sit at `dist/` root, outside both browser dirs. - ENS names are cached in popup `localStorage` (`src/shared/ens.js:18,32`), outside the state blob and outside every clearing path. Privacy only. - `window.postMessage` responses use `"*"` at `src/content/index.js:66,86` where `location.origin` is used correctly at `:44`. Same window, so no cross-origin delivery; defence in depth. - Unicode direction overrides are not neutralised in displayed `personal_sign` messages (`src/popup/views/approval.js:409,411`). - `accessList` is accepted, signed and verified but never displayed (`src/shared/approvalTx.js:62`, `approvalVerify.js:308-313` vs `approval.js:166-194`) — the one attacker-controllable signed field that is invisible. Impact bounded to gas, which is displayed. - `personal_sign` rejects plain-UTF-8 payloads that other wallets accept (`decodeHexMessage` to `getBytes` throws). - The Chrome e2e suite never clicks `#btn-confirm-send`: the popup's own Send to ConfirmTx to Sign to WaitTx path has no end-to-end coverage at all. That is how https://git.eeqj.de/sneak/AutistMask/issues/305 survived. ### Checked and genuinely fine — do not re-cover - **No lock screen is correct here.** The password is required per signing operation, so there is no decrypted material to protect. Verified `chrome.storage.session` and `chrome.storage.sync` are both empty; only `storage.local` is used, and no `storage.sync` reference exists in `src/`. - **Nothing plaintext is ever written to storage.** Wallets are pushed only after `encryptWithPassword` returns. Argon2id + XSalsa20-Poly1305 via libsodium, WASM backend enforced and loudly reported if lost. - **The password never crosses the extension messaging boundary.** Decryption happens in the popup; only the signed artifact travels. The standing floor from https://git.eeqj.de/sneak/AutistMask/issues/157 holds. - **Secret wiping is the strongest part of the code.** `revealGeneration` discards a decrypt landing after a view exit; no decrypted secret is held in any module-level variable; `show-phrase` and `export-privkey` are excluded from `RESTORABLE_VIEWS`. - **Native ETH send works end to end**, artifact verified at `eth_sendRawTransaction`. **WaitTx really is persisted** across a popup close. - **`eth_accounts` from an unconnected origin correctly returns `[]`.** - **The transaction type allowlist and canonical-bytes rebuild are excellent** — `authorizationList` (EIP-7702) stripped, forbidden, type-excluded and caught by the rebuild. - **`from` ownership is pinned at approval time and re-checked before signing.** `chainId` is verified against local state, never against anything the node said, so a lying `eth_chainId` cannot move the wallet. - **Content/page boundary**: `event.source === window` checked in all three listeners, neither content script declares `all_frames`, origin re-derived from `sender`, no `externally_connectable`. - **Blocklist artifact validation fails loud**, and `txValidation.js`/`holders.js` fail closed — unknown fee is `FEE_UNAVAILABLE` never zero; unknown holder count is null, never "low". - **`generateMnemonic` cannot be reached by the runtime debug toggle**, confirmed at bundle level. ### Not examined, and why Vendored blocklist upstream content and pinning (build-time supply chain, hash-pinned by `script/vendor-blocklist`); the `ethers` and libsodium dependencies themselves; Firefox MV2 background timer semantics beyond the shared alarm path; unit-suite quality; performance and CSS. ### Unverified - **Everything against a real network with real funds.** The RPC is stubbed in both harnesses. Nonce handling against a live mempool, real gas markets, reverts, reorgs, and fee-ceiling behaviour under real `getFeeData()` are unproven. This is the human pass PR 190 already names; the probes do not replace it. - **Chrome extension-ID stability across a checkout move** — absence of `key` verified directly, the storage-partition consequence inferred from documented Chrome behaviour. Covered by a DoD item on https://git.eeqj.de/sneak/AutistMask/issues/310. - **Firefox storage survival across an add-on remove/re-add** — `gecko.id` is stable, which is the right precondition, but no uninstall/reinstall cycle was driven. Given https://git.eeqj.de/sneak/AutistMask/issues/310 this is the single most important unverified claim. - **The exact mechanism of the killed-worker pending promise** — the hang is demonstrated; whether it is the swallowed rejection or the missing timeout is not isolated. Both need fixing. - **Whether the ungated `AUTISTMASK_ACTIVE_CHANGED` handler is reachable from a compromised renderer** — no page-reachable path found; a compromised content script was not simulated.
Author
Collaborator

Audit complete, findings filed. Closing per this issue's own definition of done — it closes when the findings exist as issues, not when they are fixed.

Filed as blockers in milestone 1.0.0: #304, #305, #306, #307, #308, #309, #310, #311, #312.

Found subsequently, by reviewing the fixes rather than by the audit itself — which is the argument for adversarial review by someone who did not write the change: #316, #317, #320, #322, #324, #331, #333.

Non-blockers: #315, #318, #323, #325, #326, #328, #329, #332.

Closed so far: #305, #306, #307, #308, #309, #316, #317.

Answer to the question this issue asks — does anything found block the 1.0.0 tag? Yes. The verdict and the full evidence are in #303 (comment). The remaining blockers are the open items in milestone 1.0.0; #190 is held WIP: until they close.

Two coverage limits from that comment are restated here so they are not lost when this issue closes: nothing was tested against a real network with real funds — the RPC is stubbed in both harnesses, so nonce handling against a live mempool, real gas markets, reverts and reorgs remain unproven — and Firefox storage survival across an add-on remove/re-add was never driven, which given #310 is the single most important unverified claim about this extension.

Audit complete, findings filed. Closing per this issue's own definition of done — it closes when the findings exist as issues, not when they are fixed. Filed as blockers in milestone 1.0.0: https://git.eeqj.de/sneak/AutistMask/issues/304, https://git.eeqj.de/sneak/AutistMask/issues/305, https://git.eeqj.de/sneak/AutistMask/issues/306, https://git.eeqj.de/sneak/AutistMask/issues/307, https://git.eeqj.de/sneak/AutistMask/issues/308, https://git.eeqj.de/sneak/AutistMask/issues/309, https://git.eeqj.de/sneak/AutistMask/issues/310, https://git.eeqj.de/sneak/AutistMask/issues/311, https://git.eeqj.de/sneak/AutistMask/issues/312. Found subsequently, by reviewing the fixes rather than by the audit itself — which is the argument for adversarial review by someone who did not write the change: https://git.eeqj.de/sneak/AutistMask/issues/316, https://git.eeqj.de/sneak/AutistMask/issues/317, https://git.eeqj.de/sneak/AutistMask/issues/320, https://git.eeqj.de/sneak/AutistMask/issues/322, https://git.eeqj.de/sneak/AutistMask/issues/324, https://git.eeqj.de/sneak/AutistMask/issues/331, https://git.eeqj.de/sneak/AutistMask/issues/333. Non-blockers: https://git.eeqj.de/sneak/AutistMask/issues/315, https://git.eeqj.de/sneak/AutistMask/issues/318, https://git.eeqj.de/sneak/AutistMask/issues/323, https://git.eeqj.de/sneak/AutistMask/issues/325, https://git.eeqj.de/sneak/AutistMask/issues/326, https://git.eeqj.de/sneak/AutistMask/issues/328, https://git.eeqj.de/sneak/AutistMask/issues/329, https://git.eeqj.de/sneak/AutistMask/issues/332. Closed so far: https://git.eeqj.de/sneak/AutistMask/issues/305, https://git.eeqj.de/sneak/AutistMask/issues/306, https://git.eeqj.de/sneak/AutistMask/issues/307, https://git.eeqj.de/sneak/AutistMask/issues/308, https://git.eeqj.de/sneak/AutistMask/issues/309, https://git.eeqj.de/sneak/AutistMask/issues/316, https://git.eeqj.de/sneak/AutistMask/issues/317. **Answer to the question this issue asks — does anything found block the 1.0.0 tag? Yes.** The verdict and the full evidence are in https://git.eeqj.de/sneak/AutistMask/issues/303#issuecomment-67399. The remaining blockers are the open items in milestone 1.0.0; https://git.eeqj.de/sneak/AutistMask/pulls/190 is held `WIP:` until they close. Two coverage limits from that comment are restated here so they are not lost when this issue closes: **nothing was tested against a real network with real funds** — the RPC is stubbed in both harnesses, so nonce handling against a live mempool, real gas markets, reverts and reorgs remain unproven — and **Firefox storage survival across an add-on remove/re-add was never driven**, which given https://git.eeqj.de/sneak/AutistMask/issues/310 is the single most important unverified claim about this extension.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/AutistMask#303