Analizzare i probe delle estensioni nel codice di LinkedIn
Un’analisi dei probe alle risorse delle estensioni, dei payload e dei limiti delle conclusioni ricavabili dal codice.
L’indagine è partita da alcune richieste insolite negli strumenti di sviluppo del browser: perché LinkedIn cercava di caricare risorse dalle estensioni di Chrome?
Ho seguito le richieste fino al codice che le generava. Questi sono gli appunti di quella sessione del 16 maggio 2026, scritti per accompagnare il mio talk sui dark pattern. Mostrano ciò che ho trovato in quella sessione; non ci dicono cosa riceva ogni visitatore o cosa succeda ai dati in seguito.
Riprodurre l’osservazione
In un browser Chromium ho aperto gli strumenti di sviluppo su linkedin.com/feed e controllato le richieste alle risorse delle estensioni. Tra le richieste fallite comparivano:
GET chrome-extension://invalid/ net::ERR_FAILED
GET chrome-extension://<32-char-id>/<some-file> net::ERR_FAILED
Seguendo l’Initiator di una richiesta sono arrivato al codice del probe. Una richiesta fallita, da sola, non prova che un’estensione sia installata. L’evidenza utile è la combinazione tra elenco delle risorse, funzione di controllo e utilizzo delle richieste risolte.
La funzione di controllo
Questa funzione è un estratto del bundle client che ho analizzato:
async function l(e) {
let t = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {},
n = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {};
if (!a() || !s()) return;
const { useRequestIdleCallback: i = !1, timeout: r = 2e3, staggerDetectionMs: l = 0 } = n,
d = async () => {
const n =
l > 0
? await (async function (e) {
const t = [];
for (const { id: n, file: i } of o) {
try {
(await fetch(`chrome-extension://${n}/${i}`)) && t.push(n);
} catch (e) {}
e > 0 && (await new Promise((t) => setTimeout(t, e)));
}
return t;
})(l)
: await c();
Array.isArray(n) &&
n.length > 0 &&
e.fireTrackingPayload('AedEvent', {
browserExtensionIds: n,
...t,
});
};
i && 'function' == typeof window.requestIdleCallback
? window.requestIdleCallback(d, {
timeout: r,
})
: await d();
}
Il ciclo legge coppie { id, file } e chiama fetch() su un URL chrome-extension://. Le richieste risolte aggiungono un ID all’array dei risultati. Un ritardo facoltativo distanzia le richieste. Due predicati, a() e s(), decidono se eseguire la funzione; il loro significato non è determinabile da questo estratto.
Quando l’array contiene risultati, il codice chiama fireTrackingPayload() con un evento AedEvent e il campo browserExtensionIds. Questo mostra come vengono preparati gli identificatori rilevati. Non spiega, da solo, l’elaborazione successiva o la loro conservazione.
L’array di input conteneva elementi di questo tipo:
{ id: "ppkpmdanppmnndopfcnpjaefkkjjiljc", file: "linkedin-overlay.95bad065.css" },
{ id: "pplkiigdcpbacikbalacopmaklichaod", file: "logo16.png" },
{ id: "pplplkmiajmkgdhjhpcibkkfkmofmcdp", file: "icons/favicon.png" },
{ id: "ppmjcikinhahlcldpndeakomkfpfedno", file: "slide-lab.html" },
{ id: "pppndnondekehijelkdnlihcfehjacfe", file: "jc.98ee31a0.css" }
Ogni elemento associa un ID di estensione al percorso di una risorsa. Il percorso può diventare obsoleto dopo un aggiornamento: un probe fallito non dimostra quindi in modo conclusivo che l’estensione sia assente.
Verificare una delle estensioni elencate
Ho scelto un’estensione per bloccare le distrazioni presente nell’elenco, l’ho installata e ho ricaricato il feed. La richiesta alla risorsa corrispondente si è risolta dove prima falliva:
chrome-extension://ppdjgkniggbikifojmkindmbhppmoell/icons/icon128.png
La catena degli initiator riportava alla stessa funzione in un bundle di LinkedIn. Il cambiamento supportava l’interpretazione secondo cui il codice verificava la raggiungibilità di una risorsa nota dell’estensione.
Il meccanismo dipende dall’esposizione della risorsa alla pagina che la richiede. Le estensioni Chrome possono dichiarare web_accessible_resources, specificando quali origini possono caricarle. Contano il comportamento del browser, il manifest, le origini consentite e il percorso della risorsa.
La documentazione di Chrome sulle risorse accessibili dal web descrive queste regole. Il probe usa richieste di risorse invece dell’API chrome.management, che richiede un permesso specifico.
Un endpoint di raccolta separato
Nella stessa sessione erano presenti anche richieste a questo endpoint:
Request URL: https://collector-pxdojv695v.protechts.net/api/v2/msft
Un bundle separato configurava i domini tramite questa funzione:
function Fu() {
try {
var t = ['protechts.net'];
Fv(t) && (Fn[mr] = t);
} catch (t) {}
try {
var n = ['/api/v2/msft'];
Fv(n) && (Fn[ms] = n);
} catch (t) {}
try {
var e = ['px-client.net', 'px-cdn.net'];
Fv(e) && (Fn[mt] = e);
} catch (t) {}
try {
var r = ['/assets/js/bundle', '/res/uc'];
Fv(r) && (Fn[mu] = r);
} catch (t) {}
try {
var h = ['/b/c'];
Fv(h) && (Fn[mv] = h);
} catch (t) {}
}
I nomi e la struttura dell’endpoint sono coerenti con l’infrastruttura dei sensori HUMAN / PerimeterX. È un’osservazione separata dai probe delle estensioni: gli estratti non dimostrano che i due meccanismi abbiano lo stesso scopo.
La funzione seguente raccoglieva proprietà del browser e del dispositivo:
function Cp(t) {
var n = (function(){ try { return hT[hR(511)] && hT[hR(511)][jc("bWVtb3J5")] } catch (t) {} })();
n && (t["BFAxGkIwMSE="] = n[jc(hR(806))],
t[hR(807)] = n[jc("anNIZWFwU2l6ZUxpbWl0")], // "jsHeapSizeLimit"
t[hR(808)] = n[jc(hR(809))]);
try {
t[hR(812)] = !!hT.Buffer,
t[hR(813)] = hT.orientation,
t["W0suQR0oJHc="] = !!hT.v8Locale,
t["KDRdPm5ZXg4="] = !!hT.ActiveXObject,
t[hR(822)] = kA(hT.openDatabase),
t[hR(827)] = hT[hR(151)]("ontouchstart") || "ontouchstart" in hT,
t[hR(828)] = kA(hT.BatteryManager) || kA(hV[hR(829)]) || kA(hV.getBattery),
t[hR(830)] = hT[hR(511)] && hT[hR(511)].navigation && hT[hR(511)][hR(831)].type,
// ...pointer:fine media query, fonts via jU(), and ~30 more
} catch (t) {}
}
Tra le chiavi decodificate ci sono memory e jsHeapSizeLimit. Il codice verifica anche funzionalità del browser, supporto touch, proprietà della navigazione e altri segnali dell’ambiente. Ho fermato l’esecuzione prima dell’invio di una richiesta e analizzato questo payload parziale:
{
"ts": 1778962041611,
"dydCbTFGQ14=": 1778962040859,
"BFAxGkIwMSE=": 8096937,
"FwdiDVFnZTo=": 4294967296,
"EwNmCVVuZzg=": 12762493,
"JnZTfGAbVU4=": "Sat May 16 2026 22:07:26 GMT+0200 (Central European Summer Time)",
"DXl4M0gfeQQ=": true,
"eEQNDj4iCzU=": "hidden",
"MDxFNnVYQgQ=": 1280,
"JxcSHWF0FS4=": 752,
"GUVsT18oaHo=": "missing",
"IxMWGWZyFyo=": 2
}
I valori descrivono la sessione acquisita: proprietà della memoria, un timestamp con fuso orario, visibilità e dimensioni del viewport. Una raccolta di questi segnali può contribuire al fingerprinting. Il campione non misura se la combinazione identifichi una persona in modo univoco.
Cosa supportano le evidenze
- Il bundle analizzato contiene una funzione condizionale che verifica URL di risorse note delle estensioni.
- Gli identificatori delle richieste risolte vengono inseriti in un payload
AedEvent. - Installare una delle estensioni elencate ha cambiato la risposta osservata al probe.
- Nella stessa sessione, un sensore separato raccoglieva proprietà del browser e del dispositivo.
Queste osservazioni non descrivono tutti gli utenti LinkedIn, tutte le estensioni o ogni impiego dei dati. Il codice del browser cambia: una verifica successiva potrebbe dare risultati diversi.
Gli estratti e il payload sono conservati dall’analisi originale. Sono evidenze da esaminare, non codice da eseguire nel browser di altre persone.