security review #6
Labels
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: sneak/AutistMask#6
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Audit this entire codebase for security bugs. Can malicious webpages inject code into our flows? Identify and list any security issues you can find in this codebase. Leave findings as a comment on this issue.
Security Review — AutistMask
Reviewed: content scripts, inpage provider, background service worker, vault, wallet, state management, popup views, manifests, and build pipeline.
CRITICAL
1. DEBUG mode ships a hardcoded mnemonic
File:
src/shared/constants.jsDEBUG = trueandDEBUG_MNEMONICis a fixed 12-word phrase.generateMnemonic()in wallet.js returns this fixed mnemonic when DEBUG is true. The build pipeline (build.js) does NOT override this constant via esbuilddefine. If this ships, every wallet uses the same mnemonic. Anyone reading the source can derive all private keys.Impact: Total loss of all funds for every user.
Fix: Remove the DEBUG mnemonic, or ensure build sets
DEBUG = falsevia esbuild define.2. postMessage uses "*" target origin
File:
src/content/index.jsResponses are posted with
window.postMessage(..., "*"). While theevent.source !== windowcheck prevents cross-origin frame injection, the "*" target means responses (including account addresses and RPC results) are visible to ALL frames on the page, including malicious iframes.Fix: Use
location.origininstead of "*" in all postMessage calls.HIGH
3. No password strength enforcement beyond 8-char minimum
Both addWallet.js and importKey.js only check
pw.length < 8. For a wallet holding funds, this is dangerously low. Argon2id INTERACTIVE params provide ~0.5s KDF work — still brute-forceable for weak 8-char passwords.Recommendation: Minimum 12 chars or integrate zxcvbn-style entropy check.
4. Approval IDs are sequential integers (predictable)
let nextApprovalId = 1;— passed in popup URL as?approval=N. A malicious page that can predict the ID could attempt to interact with the approval flow. Usecrypto.randomUUID()instead.5. eth_sign is enabled — allows signing arbitrary hashes
eth_sign signs raw 32-byte hashes (can be transaction hashes). A malicious dApp can trick users into signing fund-draining transactions. MetaMask shows strong warnings for this. Consider removing eth_sign support or adding a prominent danger warning.
6. Password sent via extension messaging in plaintext
When approving tx/sign, the password is sent popup-to-background via
runtime.sendMessage({ password }). While extension messaging is internal, consider decrypting in the popup and sending derived key/signer output instead.MEDIUM
7. connectedSites key collision risk
Uses
origin + ":" + activeAddressas key. Theoretical collision if origin contains ":" followed by an address. Use a Map with structured keys.8. No rate limiting on RPC proxy
Any page can fire unlimited RPC calls through the extension proxy, potentially exhausting the user's RPC endpoint rate limits.
9.
_metamask.isUnlocked()always returns trueDApps using this to check lock state will skip their own lock-check flows.
10. No auto-lock / session timeout
No mechanism to require re-authentication after inactivity. A stolen/unlocked browser gives full access to wallet UI, addresses, balances, and site permissions.
11. ENS cache in localStorage without integrity check
ENS names cached in localStorage. Page-context scripts can write to localStorage for web pages (not extension pages). Verify inpage script does not access this cache.
LOW / INFORMATIONAL
Summary
The DEBUG mnemonic issue (#1) is the most urgent — if this ships, it is a total loss scenario.
Starting security audit now. Will post findings here.
AutistMask Security Audit Report
Auditor: clawbot (automated)
Date: 2026-02-27
Scope: All files in
src/, manifests,package.jsonCritical Findings
C1: DEBUG Mode Hardcoded to
true— Deterministic Mnemonic in Productionsrc/shared/constants.js:1-2DEBUGis hardcoded totrueandDEBUG_MNEMONICis a fixed 12-word phrase. Inwallet.js:generateMnemonic(), whenDEBUGis true, the function returnsDEBUG_MNEMONICinstead of generating random entropy. Every wallet created by a user of this extension uses the same seed phrase.DEBUG = falsefor production builds. Better: remove the DEBUG_MNEMONIC entirely and use a build-time environment variable. Add a CI check that ensuresDEBUGisfalsein release artifacts.High Findings
H1: Content Script Origin Spoofing —
originComes From Content Script Messagesrc/content/index.js:18,src/background/index.js(message listener)origin: location.originas part of theAUTISTMASK_RPCmessage to the background script. The background script uses thismsg.originfor all permission checks (allowed/denied sites, connection status). However, Chrome'sruntime.onMessageprovidessender.originandsender.tab.urlwhich are browser-verified and cannot be spoofed by web content. The current approach trusts the content script's self-reported origin.location.originspoofed by the page, in Firefox MV2 there are historical edge cases. More importantly, if another extension injects a content script into a page, it could callchrome.runtime.sendMessagewith a forgedoriginfield. The background script does not verifysender.originorsender.tab.urlagainst the claimedmsg.origin.runtime.onMessagehandler, ignoremsg.originand instead derive the origin fromsender.origin(Chrome MV3) orsender.tab.url(Firefox MV2). This is the browser-attested origin.H2: No Sender Validation on Internal Messages
src/background/index.js(entireruntime.onMessagelistener)AUTISTMASK_TX_RESPONSE,AUTISTMASK_SIGN_RESPONSE,AUTISTMASK_APPROVAL_RESPONSE, andAUTISTMASK_GET_APPROVALmessages from any sender. It does not check that the sender is the popup page (viasender.urlorsender.tab). Any content script (running on any webpage) can send these messages.AUTISTMASK_GET_APPROVALwith a guessed/brute-forced approval ID to learn the approval details (hostname, txParams), then sendsAUTISTMASK_APPROVAL_RESPONSEwithapproved: trueto auto-approve a site connection without user interaction. Approval IDs are sequential integers starting from 1, making them trivially guessable.AUTISTMASK_GET_APPROVAL,AUTISTMASK_APPROVAL_RESPONSE,AUTISTMASK_TX_RESPONSE,AUTISTMASK_SIGN_RESPONSE), validate thatsender.urlmatches the extension's popup URL. Use cryptographically random approval IDs (e.g.,crypto.randomUUID()) instead of sequential integers.H3: Password Sent in Plaintext via
runtime.sendMessagesrc/popup/views/approval.js→src/background/index.jsruntime.sendMessage({ type: "AUTISTMASK_TX_RESPONSE", password: password }). Combined with H2, any content script that can send messages to the extension runtime can intercept the approval flow. Even without H2, the password transits through the extension messaging system.H4: Decrypted Secret Not Cleared From Memory
src/background/index.js:AUTISTMASK_TX_RESPONSE handler,src/popup/views/confirmTx.js:btn-modal-confirm handlerdecryptWithPassword(), the plaintext secret is stored in a local variable (decryptedSecret/decrypted) but never explicitly zeroed. JavaScript strings are immutable and cannot be reliably zeroed, but the variable reference persists in closure scope.Uint8Arrayfor secrets (which can be zeroed). Document the inherent limitation.Medium Findings
M1: XSS via
innerHTMLwith Partially-Escaped User Data (Settings Hostname Display)src/popup/views/settings.js:renderSiteList()renderSiteList(), hostnames from stored site permissions are interpolated into HTML via string concatenation:html += \${hostname}`. Hostnames are derived fromnew URL(origin).hostname` which normally produces safe values, but the storage could be corrupted or an edge-case URL could produce unexpected hostname values.escapeHtml(hostname)consistently. TheescapeHtmlhelper exists and is used elsewhere but not here.M2:
<all_urls>Host Permission is Overly Broadmanifest/chrome.json,manifest/firefox.json<all_urls>host permissions, giving it access to make requests to any URL. This is needed for custom RPC endpoints but grants more access than necessary for most users.<all_urls>is required (custom RPC endpoints). Consider whetheractiveTabalone would suffice for content script injection, with an optional host permission for the RPC URL.M3: No CSP Defined for Extension Pages
manifest/chrome.json,manifest/firefox.jsoncontent_security_policy. Chrome MV3 has a reasonable default CSP, but Firefox MV2 does not restrict inline scripts by default."content_security_policy": "script-src 'self'; object-src 'none'"(MV2) or the MV3 equivalent. This provides defense-in-depth against XSS.M4: Malicious RPC Endpoint Can Return Arbitrary Data
src/background/index.js:proxyRpc(), various consumersjson.error. A malicious RPC server can return crafted responses foreth_call,eth_getBalance,eth_estimateGas, etc. This could cause the UI to display incorrect balances, incorrect gas estimates (leading to stuck or overpaying transactions), or trigger unexpected behavior in ethers.js parsing.eth_call(used for ERC-20name()/symbol()) could return malicious strings that get rendered in the UI.M5:
postMessagewith"*"Target Originsrc/content/index.js:26,src/content/index.js:40window.postMessage({...}, "*")to send responses and events back to the page. Using"*"means any frame (including cross-origin iframes) on the page can receive these messages.AUTISTMASK_RESPONSEmessages and learn the user's address or intercept RPC responses.window.postMessage({...}, location.origin)or target the specific origin of the requesting page.Low Findings
L1: Sequential Approval IDs Are Predictable
src/background/index.js:nextApprovalIdnextApprovalId++). If sender validation (H2) is absent, these are trivially guessable.crypto.randomUUID()for approval IDs.L2: No Rate Limiting on Password Attempts
src/popup/views/confirmTx.js,src/popup/views/approval.jsL3:
_metamask.isUnlocked()Always Returns Truesrc/content/inpage.js:_metamask.isUnlocked()true, leaking information that the wallet is present and "unlocked" to any page, even before the user has approved a connection.falsewhen no site connection is established.L4: EIP-6963 Provider UUID is Hardcoded
src/content/inpage.js:providerInfo.uuidL5: Token Name/Symbol From RPC Not Length-Limited
src/shared/balances.js(lookupTokenInfo), various UI renderersInfo Findings
I1: Dependencies Not Pinned to Exact Versions
package.json^6.16.0). A compromised or buggy minor/patch release could be pulled in.npm audit/yarn auditin CI.I2: No Automated Security Tests
jest) exists but there are no tests specifically for security-sensitive paths (message validation, origin checking, approval flows, encryption/decryption).I3:
web_accessible_resourcesExposesinpage.jsmanifest/chrome.json,manifest/firefox.jsoninpage.jsis listed as a web-accessible resource, making it loadable by any webpage. This is necessary for Firefox injection but allows fingerprinting (any site can detect AutistMask by trying to load this resource).Summary
The most urgent issue is C1 (DEBUG mode) — if this extension is shipped as-is, every user gets the same wallet. This must be fixed before any release.
H1+H2 together are severe: a malicious page can auto-approve site connections by sending forged messages to the background script with guessed approval IDs. Fix by validating
senderin the message listener and using random approval IDs.Overall architecture is sound: Argon2id+XSalsa20 for vault encryption, proper use of ethers.js for key derivation, approval popups for sensitive operations. The main gaps are in message authentication between extension components and the DEBUG flag.
DEBUG is only for development and testing - the hardcoded mnemonic is intentional. The production build will not have DEBUG mode enabled. It even says "insecure" in a red warning in the header. Don't worry about that one.
@clawbot make me a PR to fix all of the High severity items.
Noted on DEBUG — removing from scope.
High severity items to fix:
crypto.randomUUID()(background/index.js)sender.origin/sender.tab.urlinstead ofmsg.origin(background/index.js)Creating PR now.
Make me a PR for all the Low severity ones except the L2 rate limit - that one's not real. Someone with physical access has other ways of bruteforcing the password.
Working on the Low severity fixes (excluding L2 rate limit). Fixing:
crypto.randomUUID()_metamask.isUnlocked()→ return meaningful value based on connection stateSpawning agent now.
Working on the Low severity fixes now (excluding L2 rate limiting per your note). Will create a separate PR.