← BACK TO BLOG
[Research]2026-07-109 min

Extension Detection: Active + Passive Techniques Combined

by AbuseGraph Research

Why Extension Detection Matters

Browser extensions are a strong signal for fraud workflows. Bot farms use them to:

  • Automate form filling (Selenium IDE)
  • Manage multiple identities (cookie editors)
  • Evade detection (anti-detect browsers)
  • Proxy traffic (VPN extensions)

If you can detect which extensions are installed, you can identify risky signups earlier.

The Challenge

Chrome extensions are identified by a 32-character ID (e.g., aapjdomolhkjfnjghmgdkabdalhlhjbm). There is no official API to list installed extensions — you have to infer presence.

AbuseGraph uses two complementary techniques: Active Extension Detection (AED) and Spectroscopy DOM Walk.

Technique 1: Active Extension Detection (AED)

How It Works

Chrome extensions can expose web-accessible resources. If you know the extension ID and a file path, you can attempt to fetch:

chrome-extension://aapjdomolhkjfnjghmgdkabdalhlhjbm/manifest.json
  • If the extension is installed: fetch succeeds (200)
  • If the extension is not installed: fetch fails (network error)

Implementation

async function checkExtension(id: string, file: string): Promise<boolean> {
  try {
    const response = await fetch(`chrome-extension://${id}/${file}`, {
      method: "HEAD"
    })
    return response.ok
  } catch {
    return false
  }
}

// Batch scan with Promise.allSettled (NOT Promise.all)
const results = await Promise.allSettled(
  EXTENSIONS_LIST.map(ext => checkExtension(ext.id, ext.file))
)

const detected = results
  .map((r, i) => r.status === "fulfilled" && r.value ? EXTENSIONS_LIST[i].id : null)
  .filter(Boolean)

Why Promise.allSettled?

Promise.all would reject on the first failure. Most extensions won't be installed, so most fetches fail. Promise.allSettled() keeps the batch going.

Performance

The shipped AED corpus is ~270 fraud-relevant IDs (automation, AI agents/sidebars, anti-detect, VPN, cookie tools), filtered from public LinkedIn probe research rather than a full-store dump. AbuseGraph scans in idle batches so the main thread stays responsive:

const BATCH_SIZE = 50
let offset = 0

function scanBatch() {
  const batch = EXTENSIONS_LIST.slice(offset, offset + BATCH_SIZE)
  Promise.allSettled(batch.map(checkExtension))
  offset += BATCH_SIZE
  if (offset < EXTENSIONS_LIST.length) {
    requestIdleCallback(scanBatch)
  }
}

Limitations

  • Chrome/Chromium only: Firefox and Safari use random per-install IDs
  • Web-accessible resources only: Extensions must expose resources in their manifest
  • Not exhaustive: The list is curated for fraud-relevant tools, not the entire Chrome Web Store

Technique 2: Spectroscopy DOM Walk

How It Works

Many extensions inject DOM elements (scripts, stylesheets, iframes) with chrome-extension:// URLs. Spectroscopy walks the DOM looking for these references:

function spectroscopyDomWalk(): string[] {
  const ids = new Set<string>()
  const regex = /chrome-extension:\/\/([a-z]{32})/gi

  function walk(node: Node) {
    if (node.nodeType === Node.TEXT_NODE) {
      const text = node.textContent || ""
      for (const match of text.matchAll(regex)) {
        if (match[1]) ids.add(match[1])
      }
    } else if (node.nodeType === Node.ELEMENT_NODE) {
      const el = node as Element
      for (const attr of el.attributes) {
        for (const match of attr.value.matchAll(regex)) {
          if (match[1]) ids.add(match[1])
        }
      }
      for (const child of el.childNodes) {
        walk(child)
      }
    }
  }

  walk(document.documentElement)
  return [...ids]
}

Advantages

  • Works across browsers that inject chrome-extension:// URLs into the DOM
  • No network requests — pure DOM inspection
  • Fast enough for signup-path collection

Limitations

  • Only catches injecting extensions
  • Timing-dependent — must run after injection

Combining Both Techniques

AbuseGraph runs both techniques and merges results:

export async function detectExtensions() {
  const aedIds = await scanWithAED()
  const spectroIds = spectroscopyDomWalk()
  const allIds = [...new Set([...aedIds, ...spectroIds])].sort()
  const hash = await sha256(allIds.join(","))
  return { hash, ids: allIds }
}

The Extension-Set Hash

Detected IDs are sorted and SHA-256 hashed for the identity graph:

sorted_ids = ["aapj...", "gigh...", "nkbh..."]
hash = sha256(sorted_ids.join(","))

This hash is:

  • Stable: Same extensions → same hash across sessions
  • Privacy-preserving: Can't reverse to raw extension IDs
  • Graph node: Links visitors with the same extension set

False Positives

Not everyone with a VPN extension is a fraudster. AbuseGraph handles this by:

  1. Weighting: suspicious_extensions contributes a catalog weight (not auto-block alone)
  2. Combination: One signal among 62 scored markers
  3. Consumer choice: AbuseGraph emits a verdict; you decide enforcement

The Curated Corpus

The shipped AED list includes Chrome Web Store IDs and WAR file paths when known. It is intentionally fraud-focused — filtered from public LinkedIn probe research (mdp, leopoletto, jaylane malicious-overlap) for automation, AI agents/sidebars, VPN/anti-detect, cookie editors, and form autofillers — not a dump of every store listing. Rebuild with pnpm extensions:rebuild.

Conclusion

No single technique catches everything. AED catches extensions with web-accessible resources. Spectroscopy catches injectors. Together they feed a real scored marker — visible with its weight in Review.