Extension Fingerprint Demo

A local-only awareness demo for browser extension probing. It uses fake extension IDs and sends no data anywhere.

in?
LinkedOut
privacy demo

Your browser extensions can become a fingerprint.

A website usually cannot ask Chrome for your full extension list. But it may be able to test whether specific extension resources are reachable.

One signal means little. Many signals together can reveal patterns: work tools, wallets, accessibility tools, job search tools, or sensitive proxy interests.

Demo result

All checks run locally in this browser. Nothing is sent anywhere.

Not run yet

Open the menu or press Run demo to simulate extension probing with fake extension IDs.

Check your own browser

Paste extension IDs from chrome://extensions. The page tests common exposed resource paths locally.

Needs IDs
Paste extension IDs or chrome-extension:// URLs from your own browser.

A clean result is not a guarantee. It only means the IDs you pasted did not expose the common resources tested here.

Copyable console snippet

Paste it into DevTools Console, enter your own extension IDs, and it renders a local result panel.

(async () => {
  console.clear();

  const pasted = prompt(
    "Paste extension IDs or chrome-extension:// URLs from your own browser. The check stays local.",
    ""
  );

  if (!pasted) {
    console.log("No extension IDs entered. Nothing was tested.");
    return;
  }

  const candidateResources = [
    "icon.png",
    "icons/icon.png",
    "icons/icon-128.png",
    "icons/128.png",
    "assets/icon.png",
    "images/icon.png",
    "logo.png",
    "assets/logo.png"
  ];

  function extractExtensionIds(text) {
    return Array.from(new Set((text.match(/\b[a-p]{32}\b/g) || []))).slice(0, 12);
  }

  function testResource(url, timeout = 800) {
    return new Promise((resolve) => {
      const img = new Image();
      let settled = false;

      const done = (result) => {
        if (settled) return;
        settled = true;
        img.onload = null;
        img.onerror = null;
        resolve(result);
      };

      const timer = setTimeout(() => done(false), timeout);

      img.onload = () => {
        clearTimeout(timer);
        done(true);
      };

      img.onerror = () => {
        clearTimeout(timer);
        done(false);
      };

      img.src = url;
    });
  }

  function renderPanel(results) {
    document.getElementById("extension-fingerprint-snippet-panel")?.remove();

    const foundCount = results.filter((item) => item.detected).length;
    const panel = document.createElement("section");
    panel.id = "extension-fingerprint-snippet-panel";
    panel.style.cssText = [
      "position:fixed",
      "right:16px",
      "bottom:16px",
      "z-index:2147483647",
      "width:min(420px,calc(100vw - 32px))",
      "max-height:70vh",
      "overflow:auto",
      "border:1px solid #cfd6df",
      "border-radius:10px",
      "padding:14px",
      "background:#fbfaf7",
      "color:#182033",
      "box-shadow:0 18px 48px rgba(20,26,40,.18)",
      "font:14px/1.45 system-ui,-apple-system,Segoe UI,sans-serif"
    ].join(";");

    panel.innerHTML =
      '<div style="display:flex;align-items:start;justify-content:space-between;gap:12px">' +
      '<div><strong style="font-size:16px">Extension probe result</strong>' +
      '<div style="color:#596275;margin-top:2px">Local check. No network upload.</div></div>' +
      '<button type="button" aria-label="Close" style="border:0;background:#e8edf4;border-radius:7px;padding:4px 8px;cursor:pointer">x</button>' +
      '</div>' +
      '<p style="margin:12px 0;color:#30384c">Possible reachable resources: <strong>' + foundCount + '</strong> of <strong>' + results.length + '</strong> IDs.</p>' +
      '<div style="display:grid;gap:8px">' +
      results.map((item) => {
        const status = item.detected ? "Reachable resource found" : "No exposed resource found";
        const color = item.detected ? "#8a4b00" : "#475569";
        const resource = item.reachableUrls[0] || "Tested common icon/resource paths";
        return (
          '<div style="border:1px solid #dfe4ea;border-radius:8px;padding:10px;background:#fffdf8">' +
          '<div style="font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;overflow-wrap:anywhere">' + item.id + '</div>' +
          '<div style="margin-top:5px;color:' + color + ';font-weight:700">' + status + '</div>' +
          '<div style="margin-top:4px;color:#647084;font-size:12px;overflow-wrap:anywhere">' + resource + '</div>' +
          '</div>'
        );
      }).join("") +
      '</div>' +
      '<p style="margin:12px 0 0;color:#596275;font-size:12px">A normal site cannot list all extensions, but it can test guessed extension IDs and exposed resources. Enough small signals can become a privacy fingerprint.</p>';

    panel.querySelector("button")?.addEventListener("click", () => panel.remove());
    document.body.appendChild(panel);
  }

  const ids = extractExtensionIds(pasted);

  if (ids.length === 0) {
    console.warn("No Chrome extension IDs found. Use chrome://extensions, enable Developer mode, then copy one or more IDs.");
    return;
  }

  const results = [];

  for (const id of ids) {
    const testedUrls = candidateResources.map((resource) => "chrome-extension://" + id + "/" + resource);
    const checks = await Promise.all(testedUrls.map(async (url) => [url, await testResource(url)]));
    const reachableUrls = checks.filter(([, detected]) => detected).map(([url]) => url);

    results.push({
      id,
      testedUrls,
      reachableUrls,
      detected: reachableUrls.length > 0
    });
  }

  renderPanel(results);

  const found = results.filter(r => r.detected);

  console.log("%cBrowser extension fingerprinting demo", "font-size: 18px; font-weight: bold;");
  console.log("This demo does not send data anywhere.");
  console.table(results.map((item) => ({
    id: item.id,
    testedResources: item.testedUrls.length,
    reachableResources: item.reachableUrls.length,
    firstReachableUrl: item.reachableUrls[0] || ""
  })));
  console.log("\\nSummary:");
  console.log("Tested " + results.length + " user-provided extension IDs.");
  console.log("Possible reachable resources: " + found.length);

  if (found.length > 0) {
    console.warn("Some extension resources appeared reachable. A tracker could combine this with other signals.");
  } else {
    console.log("No exposed resources were detected in the common paths tested. That does not prove the extensions are absent.");
  }

  console.log("\\nWhy this matters:");
  console.log("- A website usually cannot ask Chrome for your full extension list.");
  console.log("- But it can test guessed extension IDs and exposed resources.");
  console.log("- Some extension categories can become sensitive proxy signals: work, health, accessibility, finance, religion, politics, or job searching.");
  console.log("- This does not prove a website knows those traits. It shows how extension presence can become a proxy signal.");
})();