Is Adobe Acrobat: PDF edit, convert, sign tools safe?

Medium risk

Adobe Acrobat is medium risk. Acrobat's background worker checks every tab's hostname, not just PDF pages, against a remote list of 420 education domains. Matches go to storage key "cvEngSt" and to Adobe as a learner/non_learner tag, kept 365 days, no consent prompt.

Adobe Inc.v26.9.2.1Chrome Web Store
45Risk
Who publishes it

Adobe, Inc. - 11 other listings from the same operator, none carrying a finding

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
Adobe Inc.
Declared legal entity
Adobe, Inc.
Registered address
345 Park Ave, San Jose, CA 95110, US
Registered contact
Adobe, Inc.

Same operator - 1 listing

Published under a different store account, but sharing the registered address, contact or declared legal entity this one gave the store.

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.

acrobat.adobe.com
Also called by 4 other listings, including Adobe Bulk Download
api2.branch.io
Also called by 4 other listings, including Tonic AI

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

Adobe Acrobat classifies you as learner or non-learner from every page you load

Acrobat's background worker checks every tab's hostname, not just PDF pages, against a remote list of 420 education domains.

Matches go to storage key "cvEngSt" and to Adobe as a learner/non_learner tag, kept 365 days, no consent prompt.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-359
Source
Dynamic sandbox
What actually happens
You did this

You finish loading a page, any website, in any tab.

No PDF needs to be present, and you do not need to open the extension.

The extension did this

The extension's background worker reads the page's hostname and checks it against a list of education-related domains that Adobe serves from its own CDN.

If the hostname is on the list, the extension writes it into local storage and re-runs a learner / non-learner classification for this install.

Captured request
GEThttps://acrobat.adobe.com/dc-hosted-extension/content-signal-rules.json

Observed during dynamic analysis: 420 entries in the education/LMS category, each with a match type (domain, keyword, pattern or regex) and a character-substituted value, plus a rules object carrying uniqueDomainThreshold=3, activeDayThreshold=4, engagementSpanDays=4 and windowDays=28.

The code that does this

The navigation listener and the per-URL handler, as shipped

Readable version

The listener registration

sw_modules/student-detection/StudentDetectionModule.js
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {  if (changeInfo?.status === "complete" && tab?.url) {    this.processUrl(tab.url).catch((err) => {      loggingApi.warn({        message: "Browser student detection failed for tab URL",        error: err?.message || err?.stack,      });    });  }});

The per-URL handler

sw_modules/student-detection/StudentDetectionModule.js
async processUrl(url) {  if (!(await this.isEnabled())) return;                    // floodgate flag dc-cv-browser-student-detection  if ((await getStudySpaceExperienceFromPreference()) === false) return;  if (!(await isBrowserStudentDetectionAllowed())) return;   // false for admin-deployed installs  const hostname = HostnameNormalizer.hostnameFromUrl(url);  // every completed navigation, any site  if (!hostname) return;  const config = await this.getConfig();                     // remote content-signal-rules.json  if (!config) return;  let state = await this.windowStore.getState();             // chrome.storage.local key "cvEngSt"  const detectionConfig = await getStudentDetectionConfig();  if (StudentClassifier.isWithinLearnerRetention(state, detectionConfig.learnerRetentionDays)) return;  const match = this.matcher.findMatch(hostname, config);  state = EngagementWindowStore.pruneWindow(state, config.rules.windowDays);  if (match) {    state = EngagementWindowStore.recordEngagement(state, match.registrableDomain);  }  await this.stateManager.applyClassification(state, config, { detectionConfig });}
What's stored on your device

Record of education sites visited, kept 28 days. "tg"=classification, "rtEx"=expiry date, "evSn"=already sent to Adobe.

Location
chrome.storage.local, key "cvEngSt" (rolling window under field "rw", keyed by UTC date)
Contents
{  "tg": "non_learner",  "idTs": undefined,  "rtEx": undefined,  "evSn": false,  "cfgV": "1.0.0",  "vEn": false,  "rw": {    "2026-08-29": [      "b9fw.6wte9wx",      "jdtmtbtg61ae9wx"    ]  }}
Why you can't catch this in DevTools

The hostnames are not stored in readable form. Each character is swapped through a fixed substitution table that ships inside the extension, so the storage value looks like noise but reverses with no key.

Decoded value
[  "coursera.org",  "khanacademy.org"]
Captured request
POSThttps://sstats.adobe.com/ee/v1/interact?configId=716bebe9-4dd6-40c2-a723-7942f40515e4

Adobe Experience Edge returns an ECID identity payload, which the extension stores under the local-storage key "ECID" and replays on subsequent analytics calls.

Headers
Content-Type
application/json
Body
{  "events": [    {      "xdm": {        "eventType": "web.webpagedetails.pageViews",        "timestamp": "2026-08-30T00:17:22.481Z",        "web": {          "webPageDetails": {            "name": "DCBrowserExt:Student:Status:Updated",            "siteSection": "Acrobat Extension",            "server": "www.adobe.com"          }        },        "_experience": {          "analytics": {            "customDimensions": {              "props": {                "prop14": "non_learner"              }            }          }        }      },      "meta": {        "state": {          "cookiesEnabled": true,          "domain": "adobe.com"        }      }    }  ],  "query": {    "identity": {      "fetch": [        "ECID"      ]    }  }}
Check if you're affected

Reads the extension's "cvEngSt" state out of chrome.storage.local and reverses the character-substitution table so you can see which sites were recorded, what classification is currently held for your install, when it expires, and whether it has already been reported to Adobe's analytics endpoint.

Requires
  • Chrome with the extension installed
  • Node.js 18+ for the offline mode
decode-cvengst.js · js
// decode-cvengst.js — decode Adobe Acrobat extension "cvEngSt" engagement state.//// The extension stores the hostnames it matched against Adobe's qualifying// list under chrome.storage.local key "cvEngSt". Hostnames are written through// a fixed character-substitution table defined in common/domain-encoding.js,// so the raw storage value is not human-readable. This script reverses it.const PLAIN  = "abcdefghijklmnopqrstuvwxyz0123456789.-";const CIPHER = "tubg6lxdq8jc1m9-vw.pfhskar0oi5zn372ye4";const DECODE = {};for (let i = 0; i < PLAIN.length; i += 1) DECODE[CIPHER[i]] = PLAIN[i];function decodeDomain(token) {  return String(token)    .toLowerCase()    .split("")    .map((ch) => DECODE[ch] || ch)    .join("");}function decodeState(state) {  const parsed = typeof state === "string" ? JSON.parse(state) : state;  const window = parsed && parsed.rw ? parsed.rw : {};  const days = {};  for (const [dayKey, tokens] of Object.entries(window)) {    if (Array.isArray(tokens)) days[dayKey] = tokens.map(decodeDomain);  }  return {    classification: parsed && parsed.tg ? parsed.tg : "(none)",    identifiedAt: parsed ? parsed.idTs : undefined,    retentionExpiresAt: parsed ? parsed.rtEx : undefined,    reportedToAnalytics: Boolean(parsed && parsed.evSn),    rulesVersion: parsed ? parsed.cfgV : undefined,    enterpriseInstall: Boolean(parsed && parsed.vEn),    rollingWindow: days,  };}// --- Browser use -----------------------------------------------------------// Paste everything above into the service-worker console of the extension// (chrome://extensions -> Adobe Acrobat -> "service worker"), then run:////   chrome.storage.local.get("cvEngSt", (r) => console.log(decodeState(r.cvEngSt)));//// --- Node use --------------------------------------------------------------// Save the raw cvEngSt value to a file and run: node decode-cvengst.js state.jsonif (typeof process !== "undefined" && process.argv && process.argv[2]) {  const fs = require("node:fs");  console.log(JSON.stringify(decodeState(fs.readFileSync(process.argv[2], "utf8")), null, 2));}if (typeof module !== "undefined") module.exports = { decodeDomain, decodeState };
How to run it
  1. 1Open chrome://extensions, enable Developer mode.
  2. 2Click Acrobat's "service worker" link.
  3. 3Paste the script into the console.
  4. 4Run: chrome.storage.local.get("cvEngSt", r=>console.log(decodeState(r.cvEngSt))).
Updated 30 September 2026efaidnbmnnnibpcajpcglclefindmkaj