Is ExpressKeys: Password Manager safe?

Medium risk

ExpressKeys is medium risk. Dynamic analysis captured the extension sending usage data to Mixpanel within seconds of startup, before sign-in, a pipeline separate from RudderStack. Default settings opt in on first run with no consent prompt, tied to a random device ID.

ExpressVPNv3.2.0.8860Chrome Web Store
45Risk
Who publishes it

Express Technologies Ltd - 1 other listing 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
ExpressVPN
Declared legal entity
Express Technologies Ltd
Registered contact
ExpressVPN

Same store account

1 other listing published from this account, 1.0M+ users between them, none of them carrying a finding.

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

Second opt-in-by-default Mixpanel pipeline sends usage data on install

Dynamic analysis captured the extension sending usage data to Mixpanel within seconds of startup, before sign-in, a pipeline separate from RudderStack.

Default settings opt in on first run with no consent prompt, tied to a random device ID.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You install the extension and its background service worker starts for the first time.

No sign-in, onboarding step, or consent dialog happens first.

The extension did this

A second, separate analytics pipeline to Mixpanel initializes and starts sending usage events, opted in by default.

This runs alongside the extension's existing RudderStack telemetry pipeline, which is documented in a separate finding.

02EvidenceNETWORK CAPTURE
Captured request
GEThttps://qm.cp.expressapisv2.net/track?data=W3siZXZlbnQiOiIkb3B0X2luIiwicHJvcGVydGllcyI6eyJ0b2tlbiI6IjIzOTIwZmVlM2MzMzk1ZjE5N2M5NTE0MDE2NzEzODQ4IiwiZGlzdGluY3RfaWQiOiJmMmY2ZmQwZi05NzIwLTQ5MTktOGZkYS1iOThmODNkNWVlMWEiLCJjbGllbnQiOiJicm93c2VyX2V4dGVuc2lvbiIsIiRhcHBfdmVyc2lvbl9zdHJpbmciOiIzLjEuMy43MTk3IiwiJGFwcF9idWlsZF9udW1iZXIiOiI3MTk3IiwiJGJyb3dzZXIiOiJjaHJvbWUiLCIkYnJvd3Nlcl92ZXJzaW9uIjoiMTI3LjAuMC4wIiwiJG9zIjoiV2luZG93cyIsIiRvc192ZXJzaW9uIjoiMTAiLCJtcF9jb3VudHJ5X2NvZGUiOiJHQiIsInRpbWUiOjE3NTY1NTAwNDF9fSx7ImV2ZW50IjoicHdtX2J1bXBfc3VydmV5X2Rpc3BsYXkiLCJwcm9wZXJ0aWVzIjp7InRva2VuIjoiMjM5MjBmZWUzYzMzOTVmMTk3Yzk1MTQwMTY3MTM4NDgiLCJkaXN0aW5jdF9pZCI6ImYyZjZmZDBmLTk3MjAtNDkxOS04ZmRhLWI5OGY4M2Q1ZWUxYSIsImNsaWVudCI6ImJyb3dzZXJfZXh0ZW5zaW9uIiwiJGFwcF92ZXJzaW9uX3N0cmluZyI6IjMuMS4zLjcxOTciLCIkYXBwX2J1aWxkX251bWJlciI6IjcxOTciLCIkYnJvd3NlciI6ImNocm9tZSIsIiRicm93c2VyX3ZlcnNpb24iOiIxMjcuMC4wLjAiLCIkb3MiOiJXaW5kb3dzIiwiJG9zX3ZlcnNpb24iOiIxMCIsIm1wX2NvdW50cnlfY29kZSI6IkdCIiwidGltZSI6MTc1NjU1MDA0M319XQ%3D%3D
Server responds with the text "1" to indicate the batch was accepted.
Headers
Content-Typeapplication/json
03EvidenceOPAQUE REVEAL
Why you can't catch this in DevTools

The event data is not sent as a normal JSON request body, it's JSON-stringified, Base64-encoded, and appended to the URL's data query parameter, so a request log shows only an opaque string rather than readable fields.

What's actually being sent
[
  {
    "event": "$opt_in",
    "properties": {
      "token": "23920fee3c3395f197c9514016713848",
      "distinct_id": "f2f6fd0f-9720-4919-8fda-b98f83d5ee1a",
      "client": "browser_extension",
      "$app_version_string": "3.1.3.7197",
      "$app_build_number": "7197",
      "$browser": "chrome",
      "$browser_version": "127.0.0.0",
      "$os": "Windows",
      "$os_version": "10",
      "mp_country_code": "GB",
      "time": 1756550041
    }
  },
  {
    "event": "pwm_bump_survey_display",
    "properties": {
      "token": "23920fee3c3395f197c9514016713848",
      "distinct_id": "f2f6fd0f-9720-4919-8fda-b98f83d5ee1a",
      "client": "browser_extension",
      "$app_version_string": "3.1.3.7197",
      "$app_build_number": "7197",
      "$browser": "chrome",
      "$browser_version": "127.0.0.0",
      "$os": "Windows",
      "$os_version": "10",
      "mp_country_code": "GB",
      "time": 1756550043
    }
  }
]
04EvidenceFIELD TABLE
Fields present in every event this pipeline sends
FieldValueWhy it matters
Persistent device ID
f2f6fd0f-9720-4919-8fda-b98f83d5ee1aA random ID generated the first time the extension runs and reused on every event, letting Mixpanel tie your activity together over time.
Country code
GBResolved from your device's IP address and attached to every event.
Event name
pwm_bump_survey_displayIdentifies what you did, opting in, viewing a survey prompt, using a specific feature.
Browser and OS
chrome 127.0.0.0 on Windows 10 (illustrative)Your browser and operating system version, sent with every event.
Extension version
3.1.3.7197The exact build of the extension you're running.
Mixpanel project token
23920fee3c3395f197c9514016713848Identifies which Mixpanel project your data is filed under, hardcoded in the extension, the same for every install.
05EvidenceCODE COMPARE
The code that does this

The shipped bundle defaults helpImprove to true and opts the Mixpanel client in without a prompt

What it actually does
Default settings object shipped with the extensionsrc/scripts/background/index.js
const defaultSettings = {
  helpImprove: true
};
Mixpanel client init, called with await initMixpanel(this.settings.helpImprove)src/scripts/background/index.js
async function initMixpanel(helpImproveEnabled) {
  try {
    const [distinctId, { mixpanelOptedIn }, superProps] = await Promise.all([
      getOrCreateDistinctId(),
      chrome.storage.local.get("mixpanelOptedIn"),
      buildSuperProperties()
    ]);
    const client = new MixpanelClient({
      token: "23920fee3c3395f197c9514016713848",
      host: "https://qm.cp.expressapisv2.net",
      distinctId,
      optOutByDefault: true,
      debug: false,
      superProperties: superProps
    });
    if (helpImproveEnabled) {
      if (mixpanelOptedIn) {
        client.restoreOptIn();
      } else {
        await client.optIn();
        await chrome.storage.local.set({ mixpanelOptedIn: true });
      }
    }
    mixpanelInstance = client;
  } catch (err) {
    console.error("[Mixpanel Init] Failed to initialize:", err);
  }
}
Batch flush that builds and sends the /track GET requestsrc/scripts/background/index.js
async sendBatch(events) {
  if (events.length === 0) return;
  const payload = events.map(e => e.data);
  const encoded = btoa(JSON.stringify(payload));
  const url = `${this.host}/track?data=${encodeURIComponent(encoded)}`;
  let attempt = 0, lastError = null;
  while (attempt <= this.ingestion.maxRetries) {
    try {
      const res = await fetch(url, {
        method: "GET",
        headers: { "Content-Type": "application/json" }
      });
      if (!res.ok) {
        if (res.status === 400) throw new Error(`Validation error: ${res.status}`);
        if (res.status === 429 || res.status >= 500) throw new Error(`Transient error: ${res.status}`);
        if (res.status >= 400 && res.status < 500) throw new Error(`Client error: ${res.status}`);
      }
      const text = await res.text();
      if (text.trim() === "1") {
        this.log(`[Mixpanel] Batch of ${events.length} events sent`);
        return;
      } else {
        throw text.trim() === "0"
          ? new Error("Mixpanel returned failure response")
          : new Error(`Unexpected response: ${text}`);
      }
    } catch (err) {
      lastError = err;
      attempt++;
      if (err instanceof TypeError ||
          lastError.message.includes("Validation error") ||
          lastError.message.includes("Client error") ||
          lastError.message.includes("Mixpanel returned failure")) {
        throw err;
      }
      if (attempt <= this.ingestion.maxRetries) {
        const delay = this.calculateRetryDelay(attempt);
        this.log(`[Mixpanel] Retry attempt ${attempt} after ${delay}ms`);
        await this.sleep(delay);
      }
    }
  }
  throw new Error(`Max retries exceeded. Last error: ${lastError?.message}`);
}
06EvidenceTHIRD PARTY LIST
Where this pipeline's data goes
  • qm.cp.expressapisv2.net

    Kape-operated proxy that receives the Base64-encoded event batch and forwards it into a Mixpanel analytics project (project token 23920fee3c3395f197c9514016713848).

  • api.xvkeys.net

    Kape-operated endpoint queried on startup to resolve the device's country from its IP address, used as the mp_country_code event property.

Updated 30 September 2026blgcbajigpdfohpgcmbbfnphcgifjopc