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:
- Weighting:
suspicious_extensionscontributes a catalog weight (not auto-block alone) - Combination: One signal among 62 scored markers
- 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.