I want to describe a specific production system: 6,167 outbound HTTP probes, fired in parallel via Promise.allSettled, completing in under 200ms, with the results RSA-encrypted and attached as an HTTP header to every subsequent API call for the duration of your session. This is not hypothetical. It is LinkedIn’s Active Extension Detection system, and it has been running in production for years.
This piece is a backend engineer’s read of BrowserGate — not a privacy op-ed, but a systems analysis. We’re going to walk through the architecture, the failure modes it exploits, the data pipeline it feeds, and the distributed-systems patterns that make it both effective and difficult to detect. Along the way we’ll build a mental model of what a “browser fingerprinting backend” actually looks like at production scale, and why that model should concern anyone who runs infrastructure.
The Primitive: web-accessible Resources as an Unauthenticated Probe Endpoint
Chrome’s extension model allows developers to declare certain internal bundle files as web_accessible_resources. Once declared, those files are reachable from any origin at a stable, predictable URL:
chrome-extension://{extension-id}/{declared-file-path}
From the browser’s perspective, this is a fetch to a local resource. From a backend engineer’s perspective, this is an unauthenticated endpoint with a deterministic address scheme and a binary response: 200 if installed, network error if not.
LinkedIn’s AED system treats this as a distributed sensor array. Each extension ID is a sensor. Each fetch is a probe. Promise.allSettled is the fan-out coordinator. The result set is the telemetry payload.
The key engineering insight is that this requires zero privileges. No browser extension API access, no elevated permissions, no user gesture. The attack surface is not a vulnerability — it is a documented feature of the Chrome extension architecture. There is no patch available because there is nothing to patch.
The Probe Implementation
The deobfuscated production code (Webpack chunk 905, module 75023) runs the following core loop:
async function fetchExtensions(extensionList) {
const probes = extensionList.map(({ id, file }) =>
fetch(`chrome-extension://${id}/${file}`, { method: "GET" })
);
const results = await Promise.allSettled(probes);
const detected = [];
results.forEach((result, i) => {
if (result.status === "fulfilled") {
detected.push(extensionList[i].id);
}
});
return detected;
}
What this does well from a systems perspective:
Promise.allSettlednever short-circuits. UnlikePromise.all, a rejection from one probe doesn’t abort the rest. This is the correct choice for a fan-out read that expects partial failures by design.- Probes fire simultaneously. 6,167 requests complete in the time it takes Chrome to process one batch of microtasks plus network round-trips to localhost. In practice, this is 50–150ms depending on machine I/O load.
- There is no rate limiting on
chrome-extension://fetches. They hit the local filesystem, not a remote server. LinkedIn gets 6,167 readings for the cost of a few milliseconds of CPU.
What this reveals about the maintenance burden:
LinkedIn employs someone whose job is to identify a specific web_accessible_resource path for each of 6,167 extensions, verify it is declared in that extension’s manifest, and add it to the probe list. The list grows at roughly 12 entries per day. This is curated infrastructure. It has a maintenance cost, an owner, and presumably a roadmap.
The Second Sensor: DOM Spectroscopy
Alongside AED, a complementary system called Spectroscopy performs a full tree-walk of the live DOM, scanning every text node and element attribute for chrome-extension:// URL prefixes:
function scanDOMForExtensionArtifacts(root) {
const found = new Set();
const prefix = "chrome-extension://";
function walk(node) {
if (node.nodeType === Node.TEXT_NODE && node.textContent?.includes(prefix)) {
found.add(extractId(node.textContent));
}
if (node.nodeType === Node.ELEMENT_NODE) {
for (const attr of node.attributes) {
if (attr.value?.includes(prefix)) {
found.add(extractId(attr.value));
}
}
}
node.childNodes.forEach(walk);
}
walk(root);
return [...found];
}
Where AED detects passive installations (an extension that is installed but hasn’t modified the current page), Spectroscopy detects active modifications (an extension that has already injected DOM nodes, images, or script tags). The two systems are complementary and redundant by design.
From a systems design standpoint, this is defense-in-depth applied to surveillance. If one detection path fails — say, an extension stops declaring web-accessible resources — the other path may still catch it once it injects into the page.
The Fingerprinting Backend: APFC / DNA
Extension detection is one module inside a larger fingerprinting pipeline LinkedIn calls APFC (Anti-fraud Platform Features Collection), internally also referred to as DNA (Device Network Analysis). This pipeline collects 48 distinct signals before it fires a single extension probe:
| Signal | Backend Relevance |
|---|---|
webrtc | Leaks local LAN IP through NAT/VPN — useful for correlating device to office network |
canvas | GPU-level rendering hash; stable across browser restarts and incognito mode |
audio | AudioContext oscillator hash; hardware-level uniqueness |
fonts | System font enumeration; correlated with OS version, locale, and employer provisioning |
webgl | 65+ GPU parameters including vendor string and renderer ID |
battery | Charge level and discharge rate — fine-grained enough to identify a specific device unit |
incognito | Private browsing detection (storage quota probe) |
doNotTrack | Recorded, then excluded from hash computation |
enumerateDevices | Camera/microphone/speaker device IDs; stable hardware identifiers |
networkInfo | Connection type and downlink speed — correlates with office vs. remote vs. mobile |
automation | Detects headless browsers and Selenium drivers |
browserExtensionIds | The 6,167-entry probe list result set |
The doNotTrack entry warrants a separate note. The system reads navigator.doNotTrack, records it in the raw telemetry, and then explicitly excludes it from the composite hash used for device correlation. This is not careless omission. It is a deliberate architectural decision documented in the code. The signal is collected. The preference is noted. Tracking continues regardless.
What the Data Model Actually Looks Like
When LinkedIn attaches a fingerprint this precise to a real identity profile, the resulting data structure is qualitatively different from anything anonymous tracking produces.
A typical third-party tracking cookie sees: anonymous_visitor_a8f3k2 + rough behavioral signals.
LinkedIn’s APFC sees:
{
identity: {
name: "...",
employer: "Acme Corp",
title: "Senior Engineer",
industry: "SaaS",
connections: [...]
},
device: {
canvas_hash: "a3f9d1...",
audio_hash: "b72c44...",
webgl_renderer: "NVIDIA GeForce RTX 4080",
local_ip: "10.8.4.23",
fonts: ["Calibri", "Consolas", ...],
extensions: ["cfhdojbkjhnklbpkdaibdccddilifddb", ...]
}
}
This is a dossier, not a cookie. The identity resolution problem — which is the hard, expensive, probabilistic part of most ad-tech pipelines — is already solved. LinkedIn has your name, your employer, your title, and your professional network on file. The fingerprint is appended to known identity, not used to infer it.
How This Applies to Distributed Systems
The BrowserGate architecture is a useful reference case for thinking about several canonical distributed systems problems:
Fan-Out Reads with Expected Partial Failure
Promise.allSettled is the browser-side implementation of a scatter-gather pattern. In backend systems, the equivalent appears in:
- Parallel shard reads in distributed databases (Cassandra, DynamoDB)
- Parallel health-check fans in service mesh control planes
- Multi-region availability probes in uptime monitoring
The correct primitive in all these cases is the one that collects all results before proceeding, treating failures as data rather than exceptions. In Go:
func probeAll(targets []Target) []Result {
results := make([]Result, len(targets))
var wg sync.WaitGroup
for i, t := range targets {
wg.Add(1)
go func(idx int, target Target) {
defer wg.Done()
resp, err := http.Get(target.URL)
results[idx] = Result{Target: target, Err: err, StatusCode: statusCode(resp)}
}(i, t)
}
wg.Wait()
return results
}
The failure mode if you use the wrong primitive (Promise.all / errgroup without individual error capture) is that a single rejection aborts the collection. In LinkedIn’s case, this would mean a single uninstalled extension could prevent reporting any installed ones. Their use of Promise.allSettled is technically correct for the use case — which is itself an interesting observation about the quality of the engineering.
Idempotent Telemetry Injection
The fingerprint hash is injected as an HTTP header into every API request for the session’s duration. This is not a fire-once event but a continuous, idempotent signal. Every profile view, every search, every message send carries the device fingerprint as a header.
From an observability standpoint, this is excellent practice applied to surveillance: the server always has the current device context available without requiring the client to re-negotiate it. In legitimate backend systems, the equivalent is injecting a trace ID or session correlation token into every outbound request from a service mesh sidecar.
The lesson for backend engineers: persistent request-level context injection (whether for observability or, in this case, surveillance) is architecturally sound. The ethical question is entirely separate from the technical one.
Encrypted Payload Transmission and Opaque Debugging
The telemetry payload is RSA-encrypted before transmission to linkedin.com/li/track. From a network inspection standpoint, this is an opaque POST body — you can see the request, you cannot see its contents without the private key.
This is standard practice for sensitive telemetry in production systems. The problem it creates from a user-side debugging and auditability standpoint is significant: users cannot audit what is being sent about them in real time. Regulatory frameworks (GDPR Article 15 right of access) exist partly to address this, but they operate on timescales of weeks, not milliseconds.
For backend engineers building telemetry pipelines: RSA-encrypted opaque payloads are appropriate for sensitive data, but they should be paired with a client-accessible log or a signed receipt that lets principals verify what was collected. LinkedIn provides neither.
Staggered Fallback Mode and Anti-Detection Engineering
The probe system includes a staggerDetectionMs configuration parameter that switches from parallel batch mode to sequential mode with configurable delays between probes. This is explicitly a detection-avoidance mechanism — spreading the traffic signature to avoid triggering browser-side rate limiting or anomaly detection tooling.
In legitimate backend engineering, the equivalent pattern is jittered exponential backoff for retry logic — spreading retry load to avoid thundering herd. The mechanism is identical; the intent differs. This is a good example of how adversarial and cooperative systems share implementation patterns while diverging entirely on purpose.
Production Failure Case: The Corporate Intelligence Leak
Consider the following realistic scenario.
A mid-size SaaS company — call it Kestrel — sells a Chrome extension that provides a sales intelligence overlay on LinkedIn profiles. Kestrel has 3,400 paying customers, each of whom installs the extension to do prospecting on LinkedIn.
Kestrel does not know this, but their extension’s internal popup.html has been declared as a web_accessible_resource and added to LinkedIn’s AED probe list.
What LinkedIn now has:
- A list of every Kestrel customer’s LinkedIn profile, because each customer is logged in when the probe fires.
- Each customer’s employer, because LinkedIn knows their profile.
- Each customer’s role, seniority, and professional network.
LinkedIn also sells its own sales intelligence product: Sales Navigator. LinkedIn’s competitive intelligence team now has a reasonably complete list of Kestrel’s enterprise customer base, segmented by company size and industry, without Kestrel’s knowledge, without the customers’ consent, and with no contractual mechanism for Kestrel to detect or prevent it.
Impact:
- Customer enumeration without legal process
- Competitive intelligence gathered at scale with zero marginal cost
- No mechanism for discovery, no audit trail, no remediation path
How you would detect it: You wouldn’t, from the vendor side. Detection requires user-side tooling — specifically, a browser extension or proxy that intercepts chrome-extension:// fetch calls and logs them. The only existing mitigation is browser-level (Firefox blocks the primitive entirely; Chrome exposes it freely).
The Extension List as Special Category Data
The BrowserGate researchers documented specific categories of extensions in the 6,167-entry probe list that move this beyond aggressive anti-bot telemetry into territory with specific legal implications under GDPR Article 9 (special category data):
- Religious practice extensions: Tools for Muslim prayer times, Quran reading, kosher certification lookup, Shabbat timers. Installing these signals religious practice. Religious beliefs are Article 9 special category data.
- Health and neurodivergence tools: Extensions for dyslexia accommodation, ADHD focus management, screen magnification for low vision. Health and disability information is Article 9 special category data.
- Political orientation signals: Extensions associated with specific political movements or news sources that function as strong political orientation proxies. Political opinions are Article 9 special category data.
- Job search tools: 509 extensions for job board aggregation, resume formatting, and recruiter outreach — which, when correlated with the user’s current employer (known to LinkedIn), signal active job searching to the current employer’s network.
The legal question is not subtle: Article 9 prohibits collection of special category data without explicit consent, with narrow exceptions none of which apply to commercial fingerprinting. LinkedIn’s privacy policy contains no disclosure of this collection. There is no consent mechanism. There is no opt-out path.
“509 job search extensions in the probe list. LinkedIn knows your current employer. The intersection of those two facts, collected without consent and attached to a real identity profile, is a documented surveillance capability.”
Implementation Patterns: Browser-Side Detection and Defense
The following patterns represent what actually changes your exposure. Each is mapped to a specific threat vector.
Switching Browsers: The Architectural Fix
AED and Spectroscopy are both gated behind Chromium detection in LinkedIn’s code. If the browser does not appear to be Chromium-based, both systems exit early. Firefox neutralizes this entire attack surface at the platform level — not because Firefox has patched the chrome-extension:// scheme, but because that URL scheme is Chromium-specific. Firefox uses moz-extension://, and LinkedIn’s probe list is built entirely around Chrome extension IDs and the Chrome URL scheme.
This is the only fix that addresses the problem at the architectural level. Everything else is mitigation.
WebRTC Local IP Leak: Disable at the Protocol Level
VPNs do not protect against WebRTC-based local IP leakage. The RTCPeerConnection API bypasses the system network stack and can expose the LAN IP even when all other traffic routes through the VPN tunnel.
In Firefox:
about:config → media.peerconnection.enabled → false
In Chrome, there is no clean way to disable WebRTC without a third-party extension, which reintroduces the extension fingerprinting surface. This is one of several reasons why Firefox is the architecturally correct choice rather than a browser extension workaround.
Canvas/AudioContext Fingerprinting: Noise Injection
CanvasBlocker (Firefox) intercepts canvas and AudioContext reads before the JavaScript engine can return stable hardware-level values. It substitutes slightly randomized values that look valid but differ per session, preventing stable hash construction.
// What LinkedIn reads without CanvasBlocker:
// canvas.toDataURL() → "data:image/png;base64,iVBOR..." (stable per GPU)
// What LinkedIn reads with CanvasBlocker:
// canvas.toDataURL() → perturbed output, different each session
This breaks the canvas hash and audio hash signals. The fingerprint becomes unstable across sessions, which degrades the tracking system’s ability to correlate sessions to a persistent device identity.
Dedicated Browser Profiles: Zero-Extension Isolation
A browser profile with no extensions installed produces zero extension detections from the AED probe. This is the correct isolation primitive if you need to use Chrome: a dedicated profile used exclusively for LinkedIn, with no extensions installed.
The tradeoff is usability. Most engineers use uBlock Origin, a password manager, and several other tools that would now be absent from the LinkedIn profile. The question is whether the tracking reduction is worth the friction.
Network-Level Blocking
LinkedIn’s tracking endpoints are documented:
li.protechts.net
merchantpool1.linkedin.com
linkedin.com/li/track
uBlock Origin’s default filter lists block the first two. The li/track endpoint is more complex — it is also used for legitimate product telemetry, so blanket blocking it may break LinkedIn UI features. Network-level blocking via a local DNS resolver (Pi-hole, NextDNS) can be scoped more precisely than browser-level filter lists.
Regulatory Landscape
Fairlinked e.V. has filed proceedings against LinkedIn under the EU Digital Markets Act. The legal theory has three components:
GDPR Article 9: Collection of special category data (religious, health, political signals inferred from extension IDs) without explicit consent. The standard for special category data is explicit opt-in, not opt-out. LinkedIn has neither.
DMA Article 5(2): A designated gatekeeper may not use data obtained from third-party business users to compete against those users. LinkedIn scanning competitor tools from the browsers of their own users’ customers may fall within this prohibition. The competitive intelligence angle — mapping which companies use which competing sales tools — is the DMA framing.
DMA Article 6(10): Gatekeepers must allow users to use third-party software applications on the gatekeeper platform. Identifying and then penalizing users who use third-party tools (by enforcing ToS against scraping-adjacent behavior that is partially detected via AED results) may constitute prohibited gatekeeping.
LinkedIn was designated a DMA gatekeeper in 2023. Outcomes of the Fairlinked proceedings are pending.
Threat Model Summary
| Vector | Severity | Fix | Tradeoff |
|---|---|---|---|
| Extension enumeration (AED) | High | Firefox | No Chrome tooling |
| DOM injection detection (Spectroscopy) | Medium | Firefox | Same |
| Canvas/audio fingerprint | High | CanvasBlocker on Firefox | Slight rendering overhead |
| WebRTC local IP leak | High | Disable media.peerconnection.enabled | Breaks WebRTC calls |
| Session-persistent fingerprint injection | Medium | N/A client-side | Architectural, LinkedIn server-side |
| Competitor extension enumeration | High | Remove extensions from LinkedIn profile | Usability |
| Special category data inference | High | Firefox + no extensions | Platform switch cost |
The honest threat model assessment is this: if you use Chrome with extensions and log into LinkedIn, you are running as a sensor node in a distributed surveillance system. The data you generate is attached to your real professional identity, encrypted, and injected into every API call you make. There is no in-product disclosure, no opt-out path, and no mechanism for you to audit what has been collected.
Firefox with CanvasBlocker and WebRTC disabled is not privacy hygiene. It is the minimum-viable defense against a documented, production, actively-maintained surveillance system. The engineering question is whether the platform cost of switching is lower than the ongoing cost of the surveillance. For most engineers who have read this far, it probably is.
Code excerpts are derived from or simplified from LinkedIn’s production JavaScript bundle (Webpack chunk 905, module 75023). All signal categorizations reference EU GDPR Article 9 and the EU Digital Markets Act. Fairlinked e.V. proceedings are pending as of publication. This article reflects the author’s analysis and does not constitute legal advice.