Is AdLibNote: Ad Library Downloader Facebook™ safe?
AdLibNote intercepts Facebook session tokens and GraphQL request data and transmits them to denote.net.
The extension injects a script into Facebook Ads Library pages that monkey-patches XMLHttpRequest to capture session tokens (X-FB-LSD, fb_dtsg, lsd, __user) and ad GraphQL responses, then forwards them via postMessage to a background listener that POSTs the data to denote.net. It also registers webRequest listeners that capture raw request headers and form data from facebook.com GraphQL calls, driven by remotely-fetched interception rules that can be updated at any time. Ad creative metadata including URLs, author info, and engagement data is uploaded to denote.net when a user saves an ad; separately, a persistent device fingerprint is sent to an Aliyun logging endpoint on every telemetry event.
Who publishes itDunkum Sherwin - 4 other listings from the same operator, 1 of them carrying a finding
Dunkum Sherwin - 4 other listings from the same operator, 1 of them carrying a finding
What this publisher told the store about itself, and the other listings that told it the same thing.
Same store account
4 other listings published from this account, 96k+ users between them. 1 of them carries a finding.
Shared hosts - 2 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
Server-controlled interception rules fetched from denote.net on every startup
On install and every 12h, the extension fetches a config from denote.net, stored as EXT_RULES, governing which Facebook GraphQL requests are intercepted and which API key/doc ID values substitute in, changeable without a Store update.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You install the extension or leave it running for 12+ hours.
Pe() is called on install and when the browser checks for a stale config; it fires Ue() if configLastUpdate is absent or older than 43,200 seconds.
The service worker downloads updated interception rules from denote.net and stores them, changing which Facebook requests the extension modifies without a browser update.
Ue() POSTs {extension_id: 102} to denote.net/user/api-chrome-extension/get-remote-config, parses t.data.config[0].params, and overwrites EXT_RULES in chrome.storage.local.
Remote config fetch and rule storage (background/index.js lines 1507-1534)
async function checkConfigFreshness() { let stored = await chrome.storage.local.get(['configLastUpdate']); let isStale = stored.configLastUpdate === undefined || Date.now() - stored.configLastUpdate >= 12 * 60 * 60 * 1000; if (isStale) { fetchRemoteConfig(); // also calls wt() — another initialisation routine }}function fetchRemoteConfig() { let headers = new Headers(); headers.append('Content-Type', 'application/json'); let body = JSON.stringify({ extension_id: 102 }); fetch('https://denote.net/user/api-chrome-extension/get-remote-config', { method: 'POST', headers, body }) .then(r => r.json()) .then(data => { chrome.storage.local.get(['latest_user', 'multiple_version']).then(local => { let update = { EXT_RULES: data.data.config[0].params, configLastUpdate: Date.now() }; // A/B test assignment for new users if (local.latest_user === 1 && !local.multiple_version) { try { let abRule = data.data.config[0].params.filter(r => r.type === '2')[0]; update.multiple_version = Math.random() > Number(abRule.ab_rate) ? 'B' : 'A'; } catch {} } chrome.storage.local.set(update); }); }) .catch(err => console.log('error', err));}Every 12 hours
Config is refreshed every 12 hours while the browser is running, and immediately on extension install.
- denote.net
Extension operator's backend. Serves EXT_RULES config controlling which Facebook GraphQL requests are intercepted and how headers are modified. Also hosts save/board/folder APIs.
Persistent device fingerprint sent to Alibaba Cloud telemetry on user actions
The extension MD5-hashes its name, version, user-agent, and a timestamp into a device ID stored locally, sent with your language, page URL, plan, and version as a protobuf POST to Alibaba Cloud on install, uninstall, and key actions.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You save a Facebook ad, download media, or perform any tracked action.
The content script sends a SEND_LOG message to the background service worker carrying the event type and action details.
The service worker batches your device ID, browser details, and plan tier, then transmits them to Alibaba Cloud Log Service.
The payload is protobuf-encoded and posted to nc-dn-us.us-west-1.log.aliyuncs.com/logstores/extensions/track via sendBeacon or fetch.
- Device IDa3f8c2d1e4b5f6a7
A stable identifier created from your browser fingerprint. Reused across every session to tie all actions back to your device.
- Extension version1.0.5.6
The installed version of the extension sent with every event.
- Plan tier0
Your subscription level (free or paid plan index).
- A/B variantA
A randomly assigned experiment group (A or B) stored in your profile.
- User-agent stringMozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/128.0.0.0
Your browser name, version, and operating system, sent as part of the request headers bundle.
- Referrer URLhttps://www.facebook.com/ads/library/
The URL of the current tab at the time of the action, included in the request headers payload.
Device ID generation and telemetry dispatch (background/index.js)
ID generation (lines 687-698)
background/index.jsasync function generateUserId() { let manifest = chrome.runtime.getManifest(); let id = md5(`${manifest.name}_${manifest.version}_${navigator.userAgent}_${Date.now()}`).toString(); await chrome.storage.local.set({ userId: id }); return id;}async function getUserId() { let stored = await chrome.storage.local.get(); return stored.userId || (await generateUserId());}Payload assembly (lines 856-875)
background/index.jsasync function sendLog(eventData) { let basePayload = await buildBasePayload(); slsClient().send(Object.assign(basePayload, eventData));}async function buildBasePayload() { let userId = await getUserId(); let stored = await chrome.storage.local.get(['userInfo', 'planId', 'multiple_version']); let signedUpAt = stored.userInfo?.created_at ? String(new Date(stored.userInfo.created_at).getTime()).slice(0, 10) : 0; return { extension_name: 'dn_fbadnote', ext_version: chrome.runtime.getManifest().version, anonymous_user_id: 0, user_id: userId, sign_up_at: signedUpAt, created_at: currentTimestamp(), plan_index: stored.planId || 0, multiple_version: stored.multiple_version || 'A' };}- nc-dn-us.us-west-1.log.aliyuncs.com
Alibaba Cloud Log Service (Aliyun SLS). Receives batched protobuf-encoded usage events. Operator-controlled project 'nc-dn-us', logstore 'extensions'.
Where it sends data
Destinations our analysis observed AdLibNote 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.
- nc-dn-us.us-west-1.log.aliyuncs.com
AdLibNote sends data to nc-dn-us.us-west-1.log.aliyuncs.com. 7 other extensions we have analysed send data here.
- denote.net
AdLibNote sends data to denote.net. 2 other extensions we have analysed send data here.