- What: LinkedIn scans browser extensions without user consent
- Impact: Potential privacy risks for users of Chrome-based browsers
The Attack: How it works Every time you open LinkedIn in a Chrome-based browser, LinkedIn’s JavaScript executes a silent scan of your installed browser extensions. The scan probes for thousands of specific extensions by ID, collects the results, encrypts them, and transmits them to LinkedIn’s servers. The entire process happens in the background. There is no consent dialog, no notification, no mention of it in LinkedIn’s privacy policy. This page documents exactly how the system works, with line references and code excerpts from LinkedIn’s production JavaScript bundle. Source File All code references on this page come from a single JavaScript bundle served to every LinkedIn visitor. The filename is a content hash that changes with each deployment (e.g. 5fdhwcppjcvqvxsawd8pg1n51.js ), but the stable identifiers are: Property Value Webpack chunk ID chunk.905 Extension scan module 75023 Bundle size ~2.7 MB Framework Ember.js ( globalThis.webpackChunk_ember_auto_import_ ) The bundle is a Webpack package containing multiple modules. Three of those modules form the scanning system described below. Line numbers referenced on this page are from the December 2025 version of the bundle. They may shift between deployments, but the code structures, string literals, and module exports remain searchable by keyword. Architecture LinkedIn’s extension detection consists of three cooperating systems inside the same JavaScript bundle: System Internal Name Function APFC / DNA triggerApfc , triggerDnaApfcEvent Device fingerprinting engine. Collects 48 browser characteristics. AED AedEvent , fetchExtensions Active Extension Detection. Probes for known extensions using fetch() . Spectroscopy SpectroscopyEvent , scanDOMForPrefix Passive extension detection. Scans the DOM for evidence of extension activity. All three systems feed into the same telemetry pipeline: LinkedIn’s li/track endpoint. The Extension List At line 9571, character offset 443, inside Webpack module 75023 , there is a hardcoded array: const r = [ { id : "aaaeoelkococjpgngfokhbkkfiiegolp" , file : "assets/index-COXueBxP.js" }, { id : "aabfjmnamlihmlicgeoogldnfaaklfon" , file : "images/logo.svg" }, { id : "aacbpggdjcblgnmgjgpkpddliddineni" , file : "sidebar.html" }, // ... thousands more entries ... ]; Each entry has two fields: id : A 32-character Chrome Web Store extension ID file : A known file path inside that extension’s package, such as popup.html , icon.png , or manifest.json The file field is not incidental. Someone at LinkedIn has identified a specific internal resource for each extension that is declared as web-accessible. This is the probe target. As of December 2025, the array contained 5,459 entries . By February 2026, it had grown to 6,167 . The array alone occupies roughly 409,000 characters of source code. Stage 1: Active Extension Detection (AED) AED is a brute-force scan. It attempts to load a known file from each extension using the fetch() API. Chrome extensions can expose internal files to web pages through the web_accessible_resources field in their manifest.json . When an extension is installed and has exposed a resource, a fetch() request to chrome-extension://{id}/{file} will succeed. When the extension is not installed, Chrome blocks the request and the promise rejects. LinkedIn tests every extension in the list this way. Method 1: Parallel batch scan Lines 9573 to 9576: async function c () { const e = [], t = r . map (({ id : t , file : n }) => { return fetch ( `chrome-extension:// ${ t } / ${ n } ` ) }); ( await Promise . allSettled ( t )). forEach (( t , n ) => { if ( "fulfilled" === t . status && void 0 !== t . value ) { const t = r [ n ]; t && e . push ( t . id ); } }); return e ; } This fires all 6,222 fetch() requests simultaneously using Promise.allSettled() . Every request that resolves as "fulfilled" means that extension is installed. The function returns an array of detected extension IDs. Method 2: Staggered sequential scan Lines 9578 to 9579: async function ( e ) { const t = []; for ( const { id : n , file : i } of r ) { try { await fetch ( `chrome-extension:// ${ n } / ${ i } ` ) && t . push ( n ); } catch ( e ) {} e > 0 && await new Promise ( t => setTimeout ( t , e )); } return t ; } This alternative probes extensions one at a time with a configurable delay between each request. Failed fetches are silently caught and discarded. The delay parameter ( staggerDetectionMs ) allows LinkedIn to throttle the scan, reducing its visibility in network monitoring tools and lowering CPU impact. Which method runs Lines 9577 to 9579: const { useRequestIdleCallback : i = false , timeout : o = 2000 , staggerDetectionMs : l = 0 } = n ; const d = async () => { const n = l > 0 ? await staggeredScan ( l ) // Method 2 : await c (); // Method 1 // ... fire tracking events ... }; i && "function" == typeof window . requestIdleCallback ? window . requestIdleCallback ( d , { timeout : o }) : await d (); If staggerDetectionMs is greater than zero, Li...