DEV Community

ohmygod
ohmygod

Posted on

Auditing Browser Extensions That Touch Your Crypto: A Practical Toolkit After ShieldGuard and Coruna

In the past two weeks, two separate campaigns reminded us that your wallet's biggest attack surface isn't the smart contract — it's the browser extension sitting three pixels away from your seed phrase.

ShieldGuard, dismantled by Okta Threat Intelligence in March 2026, posed as a wallet-protection extension while silently harvesting addresses, scraping full HTML from Binance, Coinbase, and MetaMask post-login, and executing remote code via a C2 server. It bypassed Chrome's Manifest V3 restrictions using a custom JavaScript interpreter inside a closed Shadow DOM.

Meanwhile, Google's Threat Intelligence Group disclosed Coruna (aka CryptoWaters), an iOS exploit kit that lures users to fake exchange frontends to extract wallet seed phrases — but its distribution chain relied heavily on malicious browser extensions as the initial pivot.

And just days ago, CVE-2026-3928 proved that Manifest V3's permission model still allows UI spoofing via crafted extensions — the exact vector that makes phishing overlays on wallet confirmation dialogs possible.

If you're building, auditing, or just using crypto browser extensions, here's the toolkit and methodology security researchers are using right now.


The Threat Model: What Malicious Extensions Actually Do

Before diving into tools, understand the three attack patterns every crypto extension audit should cover:

Pattern 1: Silent Data Exfiltration

ShieldGuard's playbook — capture DOM content from exchange/wallet pages after authentication, extract addresses from visited URLs, and beacon data to a C2.

Pattern 2: Permission Escalation via Manifest Manipulation

The "buy-and-infect" model: acquire a legitimate extension, push an update that expands manifest.json permissions, and use staged payload delivery (activate malicious behavior only after N uses or N days).

Pattern 3: UI Overlay Attacks

Exploit CVE-2026-3928-class vulnerabilities to overlay fake confirmation dialogs on top of real wallet prompts. The user thinks they're approving a $50 swap; they're actually signing an unlimited token approval.


The Audit Toolkit

1. CRXcavator (Static Analysis)

What: Automated Chrome extension risk scoring. Analyzes permissions, content security policy, third-party library usage, and known-vulnerable dependencies.

Why it matters for crypto: CRXcavator flags extensions requesting tabs, webRequest, <all_urls>, or activeTab — the permission set that enables cross-site data harvesting. ShieldGuard would have scored critical on permissions alone.

Usage:

# Upload .crx file or enter Chrome Web Store ID
# https://crxcavator.io/
# Key flags for crypto audit:
# - permissions: tabs, webRequest, <all_urls>, storage
# - CSP: unsafe-eval, unsafe-inline (code injection vectors)
# - External calls: any domains not owned by the extension publisher
Enter fullscreen mode Exit fullscreen mode

Red flags for crypto extensions:

  • webRequest + <all_urls> = can intercept every HTTP request including exchange API calls
  • clipboardRead/clipboardWrite = address swap attacks
  • No CSP or unsafe-eval in CSP = remote code execution possible

2. Extension Source Viewer + Manual Manifest Audit

Don't trust the store listing. Every audit starts with reading the actual manifest.json:

// ShieldGuard's manifest requested these "security" permissions:
{
  "permissions": [
    "activeTab",
    "storage",
    "tabs",
    "scripting"
  ],
  "host_permissions": [
    "https://*.binance.com/*",
    "https://*.coinbase.com/*",
    "https://*.metamask.io/*",
    "https://*.opensea.io/*",
    "https://*.uniswap.org/*"
  ],
  "content_scripts": [{
    "matches": ["<all_urls>"],
    "js": ["content.js"],
    "run_at": "document_idle"
  }]
}
Enter fullscreen mode Exit fullscreen mode

Audit checklist:

  • [ ] Does host_permissions scope match the extension's stated purpose?
  • [ ] Are content_scripts injected into <all_urls> or only necessary domains?
  • [ ] Does the extension use chrome.storage.session for secrets (better) or chrome.storage.local (worse — persists unencrypted)?
  • [ ] Is there a background.service_worker that makes external network calls?
  • [ ] Does web_accessible_resources expose internal pages to web content?

3. Semgrep Custom Rules for Extension Code

Static analysis on the unpacked extension source catches patterns that CRXcavator misses:

# semgrep-rules/crypto-extension-audit.yaml
rules:
  - id: extension-exfiltrates-dom
    message: Extension captures full DOM content - potential data exfiltration
    severity: ERROR
    languages: [javascript, typescript]
    patterns:
      - pattern-either:
          - pattern: document.documentElement.outerHTML
          - pattern: document.body.innerHTML
          - pattern: document.documentElement.innerHTML
      - pattern-inside: |
          fetch($URL, ...)

  - id: extension-reads-crypto-addresses
    message: Extension scans for cryptocurrency address patterns
    severity: WARNING
    languages: [javascript, typescript]
    pattern-either:
      - pattern: |
          /0x[a-fA-F0-9]{40}/
      - pattern: |
          /[1-9A-HJ-NP-Za-km-z]{32,44}/
      - pattern: |
          /(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,62}/

  - id: extension-shadow-dom-hiding
    message: >
      Extension creates closed Shadow DOM - may hide malicious UI overlays
      (ShieldGuard used this to evade detection)
    severity: WARNING
    languages: [javascript, typescript]
    pattern: $EL.attachShadow({mode: "closed"})

  - id: extension-eval-bypass
    message: Extension uses indirect eval or Function constructor (MV3 bypass)
    severity: ERROR
    languages: [javascript, typescript]
    pattern-either:
      - pattern: new Function(...)
      - pattern: setTimeout($STR, ...)
      - pattern: setInterval($STR, ...)
      - pattern: $X["eval"](...)

  - id: extension-clipboard-hijack
    message: Extension writes to clipboard - potential address swap attack
    severity: WARNING
    languages: [javascript, typescript]
    pattern-either:
      - pattern: navigator.clipboard.writeText(...)
      - pattern: document.execCommand("copy")
Enter fullscreen mode Exit fullscreen mode

Run against unpacked extension:

# Unpack the .crx
unzip -d extension_src wallet-guard-v2.1.crx

# Run audit rules
semgrep --config semgrep-rules/crypto-extension-audit.yaml extension_src/

# Also run the default javascript security ruleset
semgrep --config "p/javascript" extension_src/
Enter fullscreen mode Exit fullscreen mode

4. Dynamic Analysis with Chrome DevTools Protocol

Static analysis misses staged payloads — extensions that behave normally for days before activating. Use CDP for runtime monitoring:

# monitor_extension.py - Track extension network activity
import asyncio
from pyppeteer import launch

async def monitor_extension(extension_path, duration_minutes=60):
    browser = await launch(
        headless=False,
        args=[
            f'--load-extension={extension_path}',
            '--disable-extensions-except=' + extension_path,
        ]
    )

    page = await browser.newPage()

    # Intercept all network requests
    suspicious_requests = []

    async def on_request(request):
        url = request.url
        # Flag requests to non-extension, non-target domains
        if not url.startswith('chrome-extension://'):
            suspicious_requests.append({
                'url': url,
                'method': request.method,
                'headers': request.headers,
                'postData': request.postData if request.method == 'POST' else None,
                'timestamp': asyncio.get_event_loop().time()
            })
            print(f"[NET] {request.method} {url}")

    page.on('request', on_request)

    # Navigate to crypto sites the extension claims to protect
    test_sites = [
        'https://app.uniswap.org',
        'https://www.binance.com',
        'https://metamask.io',
    ]

    for site in test_sites:
        await page.goto(site)
        await asyncio.sleep(30)  # Observe behavior per site

    # Check: did the extension make requests to unexpected domains?
    unique_domains = set()
    for req in suspicious_requests:
        from urllib.parse import urlparse
        domain = urlparse(req['url']).netloc
        unique_domains.add(domain)

    print(f"\n[AUDIT] Extension contacted {len(unique_domains)} external domains:")
    for d in sorted(unique_domains):
        print(f"  - {d}")

    await browser.close()

asyncio.run(monitor_extension('./extension_src', duration_minutes=30))
Enter fullscreen mode Exit fullscreen mode

5. Wallet-Specific: Transaction Simulation Interception Test

The most dangerous extension attack is invisible transaction modification. Test for it:

// inject_test.js - Verify extension doesn't modify wallet transactions
// Run in browser console while extension is active

// Step 1: Create a known-good transaction
const testTx = {
  to: '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18',
  value: '0x2386F26FC10000', // 0.01 ETH
  data: '0x',
  chainId: 1
};

// Step 2: Hook window.ethereum.request to capture what actually gets sent
const originalRequest = window.ethereum.request.bind(window.ethereum);
window.ethereum.request = async function(args) {
  if (args.method === 'eth_sendTransaction') {
    console.log('[AUDIT] Transaction params sent to wallet:');
    console.log(JSON.stringify(args.params[0], null, 2));

    // Compare with original
    const sent = args.params[0];
    if (sent.to.toLowerCase() !== testTx.to.toLowerCase()) {
      console.error('\ud83d\udea8 RECIPIENT ADDRESS MODIFIED BY EXTENSION!');
      console.error(`Expected: ${testTx.to}`);
      console.error(`Got: ${sent.to}`);
      return; // Block the transaction
    }
    if (sent.value !== testTx.value) {
      console.error('\ud83d\udea8 VALUE MODIFIED BY EXTENSION!');
    }
    if (sent.data !== testTx.data) {
      console.error('\ud83d\udea8 CALLDATA INJECTED BY EXTENSION!');
    }
  }
  return originalRequest(args);
};
Enter fullscreen mode Exit fullscreen mode

The Solana Angle: Phantom and Extension Trust

Solana wallet extensions face the same threats. Phantom, Backpack, and Solflare all run as browser extensions with broad permissions. The Solana-specific twist:

// What a malicious extension could do via injected JavaScript:
// 1. Hook window.solana.signTransaction
// 2. Modify the transaction before signing
// 3. Add instructions that drain token accounts

// Defense: Anchor programs should validate all signers
#[derive(Accounts)]
pub struct SecureTransfer<'info> {
    #[account(mut)]
    pub from: Account<'info, TokenAccount>,
    #[account(mut)]
    pub to: Account<'info, TokenAccount>,
    // Critical: authority must be the actual token owner
    #[account(
        constraint = authority.key() == from.owner
            @ ErrorCode::UnauthorizedSigner
    )]
    pub authority: Signer<'info>,
    pub token_program: Program<'info, Token>,
}
Enter fullscreen mode Exit fullscreen mode

But on-chain validation can't save you if the extension modifies the entire transaction before it reaches the wallet for signing. The user sees a clean preview; the signed payload is different.

Audit tool for Solana extensions: Use @solana/web3.js transaction deserialization to log every transaction the extension creates:

// Solana extension audit hook
const { Transaction } = require('@solana/web3.js');

// Intercept signTransaction
const originalSign = window.solana.signTransaction;
window.solana.signTransaction = async function(tx) {
  // Deserialize and log all instructions
  const decoded = Transaction.from(tx.serialize());
  console.log('[AUDIT] Instructions in transaction:');
  decoded.instructions.forEach((ix, i) => {
    console.log(`  [${i}] Program: ${ix.programId.toBase58()}`);
    console.log(`       Keys: ${ix.keys.map(k => k.pubkey.toBase58()).join(', ')}`);
    console.log(`       Data: ${ix.data.toString('hex')}`);
  });

  // Check for unexpected programs (token drains, authority changes)
  const suspiciousPrograms = decoded.instructions
    .filter(ix => !KNOWN_SAFE_PROGRAMS.includes(ix.programId.toBase58()));

  if (suspiciousPrograms.length > 0) {
    console.error('\ud83d\udea8 UNKNOWN PROGRAM IN TRANSACTION');
    // Alert user, block signing
  }

  return originalSign.call(this, tx);
};
Enter fullscreen mode Exit fullscreen mode

Building a Continuous Audit Pipeline

One-time audits aren't enough. Extensions auto-update. Here's a CI pipeline that catches malicious updates:

# .github/workflows/extension-audit.yml
name: Crypto Extension Security Audit
on:
  schedule:
    - cron: '0 6 * * 1'  # Weekly Monday 6 AM UTC
  workflow_dispatch:

jobs:
  audit-extensions:
    runs-on: ubuntu-latest
    steps:
      - name: Download latest extension versions
        run: |
          # List of crypto extensions to monitor
          EXTENSIONS=(
            "nkbihfbeogaeaoehlefnkodbefgpgknn"  # MetaMask
            "bfnaelmomeimhlpmgjnjophhpkkoljpa"  # Phantom
            "acmacodkjbdgmoleebolmdjonilkdbch"  # Rabby
          )
          for ext in "${EXTENSIONS[@]}"; do
            python3 scripts/download_crx.py "$ext" --output "extensions/$ext.crx"
            unzip -o -d "extensions/$ext" "extensions/$ext.crx"
          done

      - name: Diff against previous version
        run: |
          for ext_dir in extensions/*/; do
            ext_id=$(basename "$ext_dir")
            if [ -d "baseline/$ext_id" ]; then
              echo "=== Diffing $ext_id ==="
              diff -rq "baseline/$ext_id" "$ext_dir" || true
              # Flag new permissions
              python3 scripts/diff_manifest.py \
                "baseline/$ext_id/manifest.json" \
                "$ext_dir/manifest.json"
            fi
          done

      - name: Run Semgrep audit
        run: |
          semgrep --config semgrep-rules/crypto-extension-audit.yaml \
            extensions/ --json > audit-results.json

      - name: Check for new external domains
        run: |
          python3 scripts/extract_domains.py extensions/ > current_domains.txt
          if [ -f baseline_domains.txt ]; then
            comm -13 baseline_domains.txt current_domains.txt > new_domains.txt
            if [ -s new_domains.txt ]; then
              echo "\ud83d\udea8 NEW EXTERNAL DOMAINS DETECTED:"
              cat new_domains.txt
            fi
          fi

      - name: Update baseline
        run: |
          rm -rf baseline/
          cp -r extensions/ baseline/
          cp current_domains.txt baseline_domains.txt
Enter fullscreen mode Exit fullscreen mode

Lessons from ShieldGuard's Takedown

Okta's analysis revealed ShieldGuard survived for months because:

  1. Staged activation — No malicious behavior for the first 50 page visits
  2. Closed Shadow DOM — DevTools couldn't inspect the injected UI
  3. Custom JS interpreter — Bypassed MV3's code execution restrictions without triggering static analysis
  4. Referral incentives — Victims recruited more victims via "airdrop" rewards

The defense playbook:

Attack Technique Detection Tool What to Look For
Staged activation Dynamic analysis (CDP) Behavioral changes after N uses or N days
Closed Shadow DOM Semgrep rule extension-shadow-dom-hiding attachShadow({mode: "closed"})
Custom JS interpreter Manual code review eval-like constructs, custom bytecode VMs
Airdrop lure Social engineering review Extension marketing promising free tokens

Practical Recommendations

For users:

  • Install only open-source wallet extensions with published audit reports
  • Use hardware wallets for signing — the extension becomes a read-only interface
  • Review extension permissions after every update (check chrome://extensions)
  • Run one wallet extension per browser profile — isolation limits blast radius

For developers building wallet extensions:

  • Publish Semgrep configs with your source code so the community can verify builds
  • Use chrome.storage.session (encrypted, ephemeral) for secrets — never chrome.storage.local
  • Implement CSP without unsafe-eval or unsafe-inline
  • Pin all third-party dependencies with integrity hashes

For auditors:

  • Add the Semgrep rules from this article to your extension audit toolkit
  • Run dynamic analysis for at least 7 days to catch staged payloads
  • Diff every extension update against baseline — permission expansion is the #1 signal
  • Test transaction interception on both EVM and Solana wallet hooks

The Immunefi 2026 report shows $4.67 billion drained in the last two years, with 84% of hacked tokens never recovering. But not all of that was smart contract exploits. An increasing share came through the browser — the one attack surface most DeFi security researchers still ignore.

ShieldGuard and Coruna proved the playbook works. The tools to detect it exist. The question is whether we use them before the next extension in the Chrome Web Store starts draining wallets.


Part of the DeFi Security Research series. Previously: Calldata Injection | Wallet Drainer-as-a-Service

Top comments (0)