Is Adobe Acrobat: PDF edit, convert, sign tools safe?
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.
Who publishes itAdobe, Inc. - 11 other listings from the same operator, none carrying a finding
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.
Same store account
10 other listings published from this account, 940k+ users between them, none of them carrying a finding.
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.
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.
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'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.
The navigation listener and the per-URL handler, as shipped
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,
});
});
}
});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 });
}Record of education sites visited, kept 28 days. "tg"=classification, "rtEx"=expiry date, "evSn"=already sent to Adobe.
chrome.storage.local, key "cvEngSt" (rolling window under field "rw", keyed by UTC date){
"tg": "non_learner",
"idTs": undefined,
"rtEx": undefined,
"evSn": false,
"cfgV": "1.0.0",
"vEn": false,
"rw": {
"2026-08-29": [
"b9fw.6wte9wx",
"jdtmtbtg61ae9wx"
]
}
}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.
[ "coursera.org", "khanacademy.org" ]
| Content-Type | application/json |
{
"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"
]
}
}
}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.
// 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.json
if (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 };
- 1Open chrome://extensions, enable Developer mode.
- 2Click Acrobat's "service worker" link.
- 3Paste the script into the console.
- 4Run: chrome.storage.local.get("cvEngSt", r=>console.log(decodeState(r.cvEngSt))).