Extension Detection: How Sites Enumerate Your Installed Add-Ons and Spot Wrapper Browsers

11 min read

Probe a browser profile for extensions the way a site can, and you may be surprised by the result. A long list of visible extensions is not unusual: the password manager you forgot to disable, a few built-in browser features that ship as internal extensions, and, in an extension-based multi-account browser, the browser's own extension with several probe-able resources.

Each installed extension is a fingerprinting signal. Combine enough of them and you have a near-unique identifier. It is a detection surface that is easy to overlook while attention goes to canvas and WebGL, and it matters for three audiences: site owners who want to understand what their scripts can see, QA teams checking how a site behaves with different add-on sets, and privacy-minded users who would rather not hand out an extension list.

What We're Building

By the end of this tutorial, you'll understand:

  1. How web_accessible_resources probing works at the protocol level
  2. Why DOM injection from content scripts creates detectable artifacts
  3. The timing differences that reveal JavaScript-based overrides
  4. How to audit your own profiles for extension leaks
  5. Why native-engine browsers don't have this surface

You'll walk away with a practical checklist for auditing profiles, and a clearer understanding of why architecture matters more than features when sites actively look for extension signals.

Prerequisites

  • A browser profile in a multi-account browser (JustBrowser, Multilogin, GoLogin, or similar)
  • Basic understanding of Chrome extension structure (manifest.json, content scripts)
  • 20 minutes
  • Access to a public fingerprint viewer such as CreepJS

For the network-layer side of fingerprinting, the JA4 TLS fingerprinting guide covers how a site reads the TLS handshake.

Step 1: Understanding web_accessible_resources

Every Chrome extension has a manifest.json file. One field matters for fingerprinting: web_accessible_resources.

{
  "name": "Some Extension",
  "version": "1.0",
  "web_accessible_resources": [
    {
      "resources": ["logo.png", "inject.js", "style.css"],
      "matches": ["<all_urls>"]
    }
  ]
}

Those listed resources become accessible to any webpage via URLs like:

chrome-extension://abcdefghijklmnop/logo.png

A site can silently probe that URL. If the resource loads, meaning the fetch returns 200 instead of failing, the extension is installed. There are no permission prompts and no user interaction. The page just checks and knows.

Fingerprinting services maintain lists of extension IDs and their known resources. Probe a long list of them, and any that respond are added to the fingerprint. The combination of installed extensions is often unique enough to recognise an individual across sessions, which is exactly why privacy-minded users care about it.

Run this yourself against an extension you have installed. Open chrome://extensions, turn on Developer mode to see its ID, and pick a file its manifest lists under web_accessible_resources:

// Replace both values with an installed extension's ID and one of its listed resources
const id = '<extension-id>';
const file = '<resource-path>';
fetch(`chrome-extension://${id}/${file}`)
  .then(r => console.log(r.ok ? 'Visible to this page' : 'Not visible'))
  .catch(() => console.log('Not installed, not exposed, or blocked'));

If the page can load the file, that's the point: the page knows the extension is there. Some extensions guard against this, for example by restricting which sites can load their resources.

Step 2: The DOM Injection Problem

Content scripts are the second surface.

When an extension injects a content script, it often modifies the DOM by adding elements, modifying styles, or overriding JavaScript functions. These modifications leave artifacts.

Extension-based multi-account browsers rely on content scripts to apply identity parameters. They inject JavaScript that overrides navigator.userAgent, canvas.toDataURL, WebGLRenderingContext.getParameter. It is a standard approach.

But that injection is visible to a page:

Injected elements: Some extensions add hidden divs, iframes, or script tags. Sites can enumerate unexpected elements. A div with an ID containing "extension", or class names matching a known tool, identifies it.

CSS modifications: Content scripts sometimes inject stylesheets. Sites compute styles on known elements and compare against baselines. Unexpected CSS rules suggest extension presence.

Prototype tampering: This is the big one. When you override navigator.userAgent via JavaScript:

Object.defineProperty(navigator, 'userAgent', {
  get: () => 'overridden value'
});

The property descriptor changes. Object.getOwnPropertyDescriptor(navigator, 'userAgent') now shows different characteristics than the native implementation. Fingerprint viewers such as CreepJS report these as "lies."

So the extension modifies the DOM and prototypes to apply its settings, and those modifications become signals themselves. That is the paradox of the extension approach: a profile can look inconsistent not because of its values, but because the extension's own DOM artifacts were enumerated.

Step 3: Timing Differences on JavaScript Overrides

Native property access is fast. A JavaScript getter override adds a function call to every read, and over thousands of reads that difference can be measured.

// Timing demonstration
function measurePropertyAccess(obj, prop, iterations = 1000) {
  const start = performance.now();
  for (let i = 0; i < iterations; i++) {
    const _ = obj[prop];
  }
  return (performance.now() - start) / iterations;
}

const uaTime = measurePropertyAccess(navigator, 'userAgent');
console.log(`userAgent access: ${uaTime.toFixed(4)}ms`);
// Compare with a property nothing should override, such as screen.width

Fingerprinting services can run thousands of property access timings and compare against baselines. Consistent timing inflation across overridden properties stands out.

Some extension-based tools try to add random delays to mask the pattern, but the variance itself becomes a signal. Real browsers have consistent, low-variance timing. The architecture, not the tuning, is the limit: a JavaScript layer on top of native properties is always one more layer to measure.

Step 4: Auditing Your Profiles

Here is a practical workflow to run before relying on any profile for client work, testing or automation.

Step 4a: Probe Your Extensions

List the extensions installed in the profile and run the Step 1 snippet against each one, plus any the browser tool itself installs.

What you want: no visible extensions, or only extensions you have deliberately installed for the profile.

What you'll often see: the browser's own extension, built-in features exposing resources, and a password manager you forgot was enabled.

Step 4b: Check a Fingerprint Viewer for Tampering

Open a public fingerprint viewer and look for any section that reports prototype tampering. Common findings:

  • "WorkerNavigator mismatch": main-thread overrides don't reach Web Workers
  • "PrototypeLies": modified property descriptors
  • Timing anomalies in the consistency checks

For extension detection specifically, you're looking for any signal that JavaScript injection is occurring.

Step 4c: Inspect the DOM for Injected Elements

Open DevTools. Run:

// Look for elements with IDs or classes containing common extension patterns
document.querySelectorAll('[id*="extension"], [id*="inject"], [class*="extension"]')
  .forEach(el => console.log(el));

// Check for unexpected iframes
document.querySelectorAll('iframe').forEach(f => console.log(f.src));

// Look at scripts not from the page itself
document.querySelectorAll('script[src]').forEach(s => {
  if (!s.src.startsWith(location.origin)) console.log('External:', s.src);
});

Any elements you didn't expect point to extension injection. It might be your browser's tooling or something else, but either way it's a surface.

Step 4d: Test Property Access Timing

function timeProperty(obj, prop) {
  const runs = [];
  for (let i = 0; i < 100; i++) {
    const start = performance.now();
    for (let j = 0; j < 1000; j++) { const _ = obj[prop]; }
    runs.push(performance.now() - start);
  }
  return {
    avg: (runs.reduce((a,b) => a+b) / runs.length).toFixed(3),
    variance: (Math.sqrt(runs.map(x => Math.pow(x - runs.reduce((a,b) => a+b)/runs.length, 2)).reduce((a,b) => a+b) / runs.length)).toFixed(3)
  };
}

console.log('navigator.userAgent:', timeProperty(navigator, 'userAgent'));
console.log('navigator.platform:', timeProperty(navigator, 'platform'));
console.log('screen.width:', timeProperty(screen, 'width'));

Native properties show low, consistent times with low variance. Overridden properties show higher times and/or higher variance. If navigator.userAgent is consistently much slower than screen.width, something is modifying it.

Step 5: Why Native Engines Don't Have This Surface

This is where the architecture distinction becomes concrete.

Native-engine multi-account browsers, JustBrowser among them, don't use extensions or content scripts. They modify the Chromium source code at the C++ level.

There is no extension manifest, so no web_accessible_resources to probe. There are no content scripts injecting DOM elements and no JavaScript overrides creating timing differences.

When a native-engine browser reports a canvas fingerprint, it generates that value from the modified rendering pipeline. As far as JavaScript can tell, the value is simply what the browser reports. There is no override to time because the value was never replaced.

JustBrowser is our product, so weigh that accordingly, but the point is architectural: extension-based tools have a surface that native engines don't.

Native engines have their own costs. Keeping up with Chromium updates takes continuous work, keeping all 40+ identity parameters consistent with each other takes care, and building APIs that work well with Playwright and Puppeteer isn't trivial. That is why many tools take the easier extension route and accept the trade-off.

Common Errors and Fixes

Error: Extensions show up as visible but I disabled them

Cause: Browser built-ins, PDF viewers, or features that ship as internal extensions. Some tools bundle their own extensions even when they describe themselves as native.

Fix: Check your browser's settings for internal features. Disable the PDF viewer, translate features, and anything else that might be extension-based. If the tool itself is detected, it isn't native-engine regardless of how it is described.

Error: A fingerprint viewer shows prototype tampering although the browser claims native support

Cause: Mixed architecture. Some tools use native modification for some parameters and extension injection for others, and the injected parameters get flagged.

Fix: Run the timing tests from Step 4d. If some properties are slow and others aren't, you have a hybrid architecture, and the slow properties are JavaScript overrides.

Error: DOM shows injected elements I can't identify

Cause: Could be browser extensions you forgot about, the browser's own infrastructure, or an extension you didn't install intentionally.

Fix: Disable all extensions. Test again. If elements persist, they come from the browser itself. If they disappear, re-enable extensions one at a time to identify the culprit.

Error: Timing tests look fine but a site still treats the session as unusual

Cause: Extension detection is one surface among many. IP quality, behavioral signals, JA4 TLS fingerprints and cross-session correlation all contribute, and bot-management vendors cross-reference them. If you operate a site yourself, ClickzProtect is an example of a product that looks at several signals together for click quality.

Fix: On a site you run, check your bot-management rules and allowlist your test traffic, so the test measures the site rather than the rules. On someone else's site, how it treats a session is its call: follow its terms, use its API where automated access is wanted, and where a platform has restricted an account, use its appeal route rather than a technical workaround.

Next Steps

Once you know what your profiles expose through extensions:

  1. Review the rest of what a page can read: TLS fingerprints, WebGL, audio, fonts
  2. Check timezone and language against the proxy location, so a test reflects the region you meant to test
  3. Document verified configurations: create profile templates from setups you have checked

For teams running automation, the Playwright/Puppeteer integration guide covers driving a profile through the local API, which brings its own considerations beyond extension enumeration.

Extension detection is standard in fingerprinting now. Profiles on an architecture without extension surfaces simply have less to expose.

Frequently Asked Questions

What are web_accessible_resources and why do they matter for fingerprinting?

web_accessible_resources is a Chrome extension manifest field that lists files accessible to web pages. Sites can probe these URLs (chrome-extension://[extension-id]/[file]) to check if specific extensions are installed. If the resource loads, the extension is present. This creates a fingerprinting vector because each installed extension combination is relatively unique.

Can Manifest V3 extensions still be detected through resource probing?

Yes, but with limitations. Manifest V3 requires extensions to explicitly declare accessible resources and can restrict them to specific URL patterns. However, many extensions still expose resources for functionality. The detection shifts toward DOM injection patterns, timing differences, and behavioral analysis rather than pure resource enumeration.

Why do extension-based multi-account browsers expose more extension signals?

Extension-based multi-account browsers apply their identity settings through content scripts, which are themselves extensions. The extension's own web_accessible_resources can be probed, its DOM modifications leave artifacts, and its JavaScript overrides create timing signatures. A native-engine browser applies the same settings inside the engine, so there is no extension surface to probe.

How can I check whether my browser leaks extension signals?

Probe the extensions installed in the profile with a short fetch snippet, check a fingerprint viewer such as CreepJS for prototype tampering, and inspect your DOM for injected elements or modified prototypes. Look for elements you didn't create, timing anomalies in property access, and unexpected CSS that suggests content script injection.


Try JustBrowser

A multi-account browser for teams, built on a custom Chromium engine — not extension-based. Native C++ engine patches at the canvas / WebGL / audio / font / screen layer provide 40+ identity parameters, and each profile keeps its own cookies, storage and proxy. Local REST API + CDP for Playwright, Puppeteer, Selenium. $9.99/month or $99.99/year. 7-day free trial, card required — cancel any time in the seven days and you are not charged. Unlimited profiles.

Get started → · How it differs from Multilogin / GoLogin / AdsPower

Ready to manage multiple accounts?

Seven days free, then $9.99/month — one plan, everything included.

We'd like to use Google Analytics, a Google service, to understand how our website is used. It sets two cookies in your browser and runs only if you click Accept. You can change your choice at any time with Cookie settings. Cookie Policy

Sign-in cookies and the cookie that remembers this choice are always on; the website needs them to work.

Google Analytics, a Google service, helps us understand how our website is used. It sets two cookies, _ga and _ga_TVZHQ99TZW. It is now onoff in this browser. If your browser sends a Global Privacy Control or Do Not Track signal, it stays off. Cookie Policy