Skip to content

Browser privacy · Independent investigation ·

Inside LinkedIn’s extension detection

Reverse-engineering a browser signal from failed requests to a conditional telemetry pipeline.

The captured client was designed to probe 4,934 browser extensions and report positive results. Both detectors returned empty arrays in my recorded test. I mapped the reporting path, but did not observe an extension ID being transmitted.

I traced production JavaScript with DevTools, checked the detector results at runtime, and built a reproducible Python analysis of the exported catalogue.

Full research on GitHub
Unique extension IDs in the target catalogue
4,934
Detection mechanisms traced in the client
2
IDs returned by each recorded detector execution
0

01

The requests that started it

While browsing LinkedIn, I noticed repeated chrome-extension://invalid/ failures in Chrome DevTools. Following a request’s initiator first led to a wrapper around window.fetch. Inspecting its arguments and callers revealed something more specific: the page was testing known extension IDs and resource paths.

I used breakpoints, result logging, and prototype inspection to connect the requests to the detection code, then exported its target catalogue. The 175 retained probe records all match the export. The full count of 4,934 comes from the catalogue and source loops.

Original Chrome DevTools console showing repeated failed chrome-extension://invalid/ requests and their source locations
Original, unchanged console capture. The error counter does not measure the catalogue size. The visible AdBlock tab is browser interface context, not a detector result.

02

Two ways to look for an extension

The two functions obtain their signals differently. Neither provides a complete inventory of installed extensions.

Active probing: request a known resource

The active detector builds chrome-extension:// URLs from each catalogue ID and path. Its parallel branch uses Promise.allSettled and records fulfilled, non-undefined fetch results without checking Response.ok or reading the response body. A second branch runs sequentially with an optional delay. The requestIdleCallback timeout controls scheduling, not an individual fetch’s network timeout.

Passive scanning: read references in the page

The passive detector walks document text nodes and element attributes for chrome-extension:// references. It takes the first token after that prefix in each string. The captured implementation does not validate ID format, remove duplicates, or establish who inserted a reference. It also does not explicitly traverse shadow roots or iframe documents. A matching reference is a signal, not proof of installation.

03

The result that limits the claim

Positive active results feed AedEvent; passive results feed SpectroscopyEvent. Both call sites check that the result is an array with at least one item before emitting browserExtensionIds. In the recorded executions, both arrays were empty, so neither guard would emit its extension event.

An empty result does not establish that no extensions were installed. Chrome restricts resource access by default; origin rules, resource changes, and dynamic IDs can prevent a fixed-ID probe from succeeding. The passive scan has its own coverage limits. No positive-control experiment was retained, so this study does not measure detection accuracy.

DETECTED: []       COUNT: 0
SPECTROSCOPY: []   COUNT: 0
Condensed transcription of the recorded console output; these are the two inspected result points.
Original Chrome DevTools capture of the nonempty-array guard before AedEvent is emitted
The original breakpoint capture shows the AedEvent guard and browserExtensionIds field. It establishes the code location; it does not show the runtime array value.

04

Following the reporting path

From fireTrackingPayload, I followed the shared tracking client through event envelopes, route selection, queues, request packaging, and the final fetch helper. Runtime lookups assigned both extension event names to normal-priority. Separate feature flags can still select legacy handling, the newer route, or both; a destination lookup alone does not prove that an event used it.

The inspected request manager had a batch size of 30 and a flush debounce of 10,000 ms. The transport helper tried fetch with keepalive enabled and retried without it on rejection. Despite the name beaconFunc higher in the chain, this path reached fetch, not evidence of navigator.sendBeacon.

At the final transport step, I inspected a real request containing LixTreatmentsEvent, with JSON packaging and gzip configuration. This provided a runtime example of the shared transport carrying a different event.

  1. 01
    Detector resultActive resource probes or document references
  2. 02
    Nonempty guardOnly positive arrays enter an extension event
  3. 03
    Tracking clientEnvelope, route selection, queue and batching
  4. 04
    Shared transportRequest packaging, retry handling and fetch
Reconstructed control flow. The recorded empty arrays stopped at stage 2; stages 3–4 describe the shared reporting infrastructure.

05

Measuring the catalogue

The export contains one resource path for each of 4,934 unique IDs and 2,487 distinct paths. It contains no product names, versions, installation counts, or reasons for inclusion. I analyzed the JSON offline with Python and kept direct measurements separate from tentative filename hints.

PNG files account for almost half the targets. The most frequent exact path is icons/icon16.png, used 407 times. Generic paths cannot reliably tell us an extension’s purpose, and a JavaScript target does not mean that script was executed by this detector.

The research package associates ten deliberately selected IDs with public product listings; the other 4,924 identities remain unresolved. That small sample is not representative. I did not turn filename matches into estimates of how many targets serve a particular purpose.

Resource suffixes in the captured catalogue. Shares are rounded independently.
ResourceTargetsShare
PNG2,30846.78%
JavaScript (.js)85417.31%
HTML (.html)80816.38%
CSS50710.28%
Other suffixes4579.26%

06

Privacy implications and LinkedIn’s explanation

My privacy concern is the information a positive result could reveal about someone’s software choices. An extension’s purpose may suggest interests or working habits, but does not establish personal traits or why it was installed. That is a potential implication of the mechanism, not an observed use of the data. It makes the scope of collection, retention, and access worth examining.

LinkedIn’s stated purpose

In a statement published by BleepingComputer on 3 April 2026, LinkedIn said it detects extensions to address scraping and terms violations, improve technical defenses, and investigate unusually heavy retrieval of members’ data. It said it does not use these signals to infer sensitive information about members. This is the company’s explanation, not a finding verified by my browser investigation.

Policy context

LinkedIn’s Privacy Policy lists browser add-ons among device information (§1.5) and describes security, fraud prevention, and investigations as data uses (§2.9). These disclosures provide context; they do not specify the captured catalogue’s membership or the retention and handling of these particular events.

07

What I take from the investigation

The difficult part was keeping each step’s meaning intact: a target is not a detection, a detection is not an emitted event, and a configured route is not proof of delivery. Moving past the first wrapper, inspecting inherited methods, and connecting source code to recorded runtime values made the system understandable without overstating the result.

This is a client-side investigation of my own signed-in session, documented on 12 September 2026. I conducted the DevTools work with AI-assisted guidance; AI also assisted with source tracing, catalogue analysis, and drafting. I checked the analysis against the retained artifacts. Antoine Vastel’s January 2026 analysis at Castle describes prior work on this mechanism; I make no claim of first discovery.

The record has no complete HAR, controlled positive result, or backend access. It cannot establish retention, profiling, sharing, or the reason a particular extension was targeted. Those remain questions for the operator. What it does demonstrate is a reproducible catalogue analysis and a traceable account of the client’s conditional reporting design, including the negative result.

08

Evidence and source material

Selected artifacts from the investigation are available below. The catalogue is a list of possible targets, not my installed extensions. The 175 probe records match it exactly. Personal session payloads and unrelated console content are excluded.

Full research on GitHub

The catalogue, source excerpts, and screenshots are retained as reference material for technical commentary. No affiliation with LinkedIn or a new license over its code is claimed. The catalogue digest identifies the captured file’s bytes; it does not independently authenticate their origin.

Back to projects