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

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
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.

02EvidenceNETWORK CAPTURE
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.
03EvidenceCODE COMPARE
The code that does this

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

What it actually does
The listener registrationsw_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 handlersw_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 });
}
04EvidenceSTORAGE DUMP
What's stored on your device

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

Locationchrome.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"
    ]
  }
}
05EvidenceOPAQUE REVEAL
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.

What's actually being sent
[
  "coursera.org",
  "khanacademy.org"
]
06EvidenceNETWORK CAPTURE
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-Typeapplication/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"
      ]
    }
  }
}
07EvidenceARTIFACT
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.

RequiresChrome with the extension installedNode.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.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 };
How to run it
  1. 1
    Open chrome://extensions, enable Developer mode.
  2. 2
    Click Acrobat's "service worker" link.
  3. 3
    Paste the script into the console.
  4. 4
    Run: chrome.storage.local.get("cvEngSt", r=>console.log(decodeState(r.cvEngSt))).
Updated 30 September 2026efaidnbmnnnibpcajpcglclefindmkaj