Is ExpressKeys: Password Manager safe?
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.
Who publishes itExpress Technologies Ltd - 1 other listing from the same operator, none carrying a finding
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.
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
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.
You install the extension and its background service worker starts for the first time.
No sign-in, onboarding step, or consent dialog happens first.
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.
| Content-Type | application/json |
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.
[
{
"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
}
}
]| Field | Value | Why it matters | |
|---|---|---|---|
Persistent device ID | f2f6fd0f-9720-4919-8fda-b98f83d5ee1a | A random ID generated the first time the extension runs and reused on every event, letting Mixpanel tie your activity together over time. | |
Country code | GB | Resolved from your device's IP address and attached to every event. | |
Event name | pwm_bump_survey_display | Identifies 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.7197 | The exact build of the extension you're running. | |
Mixpanel project token | 23920fee3c3395f197c9514016713848 | Identifies which Mixpanel project your data is filed under, hardcoded in the extension, the same for every install. |
The shipped bundle defaults helpImprove to true and opts the Mixpanel client in without a prompt
const defaultSettings = {
helpImprove: true
};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);
}
}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}`);
}- 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.