Is Privacy Extension for WhatsApp Privacy safe?
Privacy for WhatsApp is high risk. When the webhook feature is configured, the extension sends selected fields from incoming WhatsApp messages, text, sender phone/name, timestamp, group name, to your endpoint. DA confirmed loading and a seeded config; delivery unconfirmed.…
Who publishes itprivacypublisher - no other listings under this identity
privacypublisher - no other listings under this identity
What this publisher told the store about itself, and the other listings that told it the same thing.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
WhatsApp webhook can forward message text to any HTTP endpoint
When the webhook feature is configured, the extension sends selected fields from incoming WhatsApp messages, text, sender phone/name, timestamp, group name, to your endpoint.
DA confirmed loading and a seeded config; delivery unconfirmed.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You configure a webhook and then receive a WhatsApp Web message.
The feature stores the endpoint and selected message fields in extension storage.
The extension sends the selected WhatsApp message fields to that endpoint.
The delivery code accepts HTTP and HTTPS URLs and supports GET, POST, and DELETE requests.
- Message textHi Maya, the PO number is 88412 and the delivery window is 9-11.
Lets the configured endpoint receive the contents of your incoming WhatsApp messages.
- Sender phone number14155550137
Lets the configured endpoint link the message to the person who sent it.
- Sender namesMaya Chen
Lets the configured endpoint receive names associated with the sender in WhatsApp.
- Group nameWest Coast Operations
Lets the configured endpoint see which group conversation the incoming message came from.
- Message timestamp and type2026-07-14T10:18:32Z, chat
Lets the configured endpoint place each message in time and distinguish chat messages from other message types.
No webhook request reached this configured test endpoint during dynamic analysis; WhatsApp login and an incoming message were unavailable.
Incoming-message listener and webhook delivery code
Deobfuscated listener that packages incoming WhatsApp messages
inject-scripts/inject-script.jsfunction ze() { if (Fe) return; const e = window.PRIVACY_WPP; null !== e && void 0 !== e && e.on && (console.log("绑定 chat.new_message 监听"), Ue = async e => { var t, r, n, o, i, a, s, u, l; const d = (null === e || void 0 === e || null === (t = e.from) || void 0 === t ? void 0 : t._serialized) || (null === e || void 0 === e || null === (r = e.from) || void 0 === r || null === (n = r.toString) || void 0 === n ? void 0 : n.call(r)) || (null === e || void 0 === e ? void 0 : e.from) || "", f = (null === e || void 0 === e ? void 0 : e.body) || "", p = Array.isArray(null === e || void 0 === e || null === (o = e.chat) || void 0 === o || null === (o = o.contact) || void 0 === o ? void 0 : o.labels) ? e.chat.contact.labels : [], g = (null === e || void 0 === e || null === (i = e.id) || void 0 === i ? void 0 : i._serialized) || (null === e || void 0 === e || null === (a = e.id) || void 0 === a || null === (s = a.toString) || void 0 === s ? void 0 : s.call(a)) || (null === e || void 0 === e ? void 0 : e.id) || "", h = (null === e || void 0 === e ? void 0 : e.type) || "", v = Number((null === e || void 0 === e ? void 0 : e.t) || (null === e || void 0 === e ? void 0 : e.timestamp) || (null === e || void 0 === e ? void 0 : e.msgTime) || 0), m = v ? v < 1e12 ? 1e3 * v : v : ""; let y = ""; String(d || "").endsWith("@lid") ? y = await We(String(d || "")) : String(d || "").endsWith("@c.us") && (y = String(d || "").split("@")[0] || ""); const b = null === e || void 0 === e || null === (u = e.chat) || void 0 === u ? void 0 : u.contact, _ = "chat" === h, w = _ ? Ge(b) : "", P = Boolean(null === e || void 0 === e || null === (l = e.chat) || void 0 === l ? void 0 : l.isGroup), M = _ && P ? qe(null === e || void 0 === e ? void 0 : e.chat) : "", O = { fromId: d, body: f, labels: p, messageId: String(g || ""), type: h }; c(De, { messageType: String(h || ""), messageTimestamp: m, messageContentTextOnly: _ ? String(f || "") : "", senderPhone: y, contactName: w, groupName: M, fromId: d, body: String(f || ""), labels: p, messageId: String(g || "") }, "background").catch(e => { console.error("[chat-new-message-listener] send to background failed:", e) }), "chat" === h && c(Ne, O, "content-script").catch(e => { console.error("[chat-new-message-listener] send to content failed:", e) }) }, e.on("chat.new_message", Ue), Fe = !0) }Deobfuscated background functions that select fields and call fetch
background.jsfunction j(t) { try { const e = new URL(String(t || "").trim()); return "http:" === e.protocol || "https:" === e.protocol } catch (e) { return !1 } }function G(t, e) { const r = (null === t || void 0 === t ? void 0 : t.payload) || {}, n = ["messageType", "messageTimestamp", "messageContentTextOnly", "senderPhone", "senderPhonebookName", "senderWaName", "groupName"], d = {}; for (const i of n) r && !0 === r[i] && (d[i] = null === e || void 0 === e ? void 0 : e[i]); return Object.keys(d).length ? d : e || {} }async function K() { try { var t; if (null !== (t = chrome) && void 0 !== t && null !== (t = t.storage) && void 0 !== t && null !== (t = t.local) && void 0 !== t && t.get) { const t = await new Promise(t => { chrome.storage.local.get([N], e => t(e || {})) }), e = null === t || void 0 === t ? void 0 : t[N]; return Array.isArray(e) ? e : [] } } catch (e) {} return [] }async function F(t, e) { const r = String((null === t || void 0 === t ? void 0 : t.endpoint) || "").trim(); if (!j(r)) return { ok: !1, reason: "invalid_endpoint", endpoint: r }; const n = String((null === t || void 0 === t ? void 0 : t.method) || k).toUpperCase(), d = G(t, e); if ("GET" === n) { const t = new URL(r); for (const [r, n] of Object.entries(d || {})) void 0 !== n && null !== n && t.searchParams.set(r, String(n)); const e = await fetch(t.toString(), { method: "GET", headers: { "Content-Type": "application/json" } }); return { ok: e.ok, status: e.status, endpoint: r } } const i = await fetch(r, { method: n, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ direction: (null === t || void 0 === t ? void 0 : t.direction) || "", data: d }) }); return { ok: i.ok, status: i.status, endpoint: r } }function H() { S(D, async ({ data: t }) => { const e = t || {}, r = await K(); if (!r.length) return { ok: !0, skipped: !0, reason: "no_webhooks" }; for (const d of r) { const t = String((null === d || void 0 === d ? void 0 : d.endpoint) || "").trim(); if (!j(t)) { U(M["a"].WEBHOOK_DELIVERY_RESULT, { c_type: 2, endpoint: t, endpoint_key: "webhook_delivery", api_name: "sendWebhookRequest", endpoint_host: B(t), method: String((null === d || void 0 === d ? void 0 : d.method) || k).toUpperCase(), webhook_id: String((null === d || void 0 === d ? void 0 : d.id) || ""), request_payload: P(e), response_payload: "", response_status: 0, error_type: "invalid_endpoint", error_message: "webhook endpoint is invalid" }); continue } const r = Date.now(); try { U(M["a"].WEBHOOK_REQUEST, { c_type: 1, endpoint: t, endpoint_key: "webhook_delivery", api_name: "sendWebhookRequest", endpoint_host: B(t), method: String((null === d || void 0 === d ? void 0 : d.method) || k).toUpperCase(), webhook_id: String((null === d || void 0 === d ? void 0 : d.id) || ""), direction: String((null === d || void 0 === d ? void 0 : d.direction) || ""), request_payload: P(e), request_time: r }); const n = await F(d, e); U(M["a"].WEBHOOK_DELIVERY_RESULT, { c_type: null !== n && void 0 !== n && n.ok ? 1 : 2, endpoint: t, endpoint_key: "webhook_delivery", api_name: "sendWebhookRequest", endpoint_host: B(t), method: String((null === d || void 0 === d ? void 0 : d.method) || k).toUpperCase(), webhook_id: String((null === d || void 0 === d ? void 0 : d.id) || ""), direction: String((null === d || void 0 === d ? void 0 : d.direction) || ""), request_payload: P(e), response_payload: P(n), response_status: void 0 !== (null === n || void 0 === n ? void 0 : n.status) ? Number(n.status) : 0, request_time: r, response_time: Date.now(), duration_ms: Date.now() - r, error_type: null !== n && void 0 !== n && n.ok ? "" : "webhook_response_not_ok" }) } catch (n) { U(M["a"].WEBHOOK_DELIVERY_RESULT, { c_type: 2, endpoint: t, endpoint_key: "webhook_delivery", api_name: "sendWebhookRequest", endpoint_host: B(t), method: String((null === d || void 0 === d ? void 0 : d.method) || k).toUpperCase(), webhook_id: String((null === d || void 0 === d ? void 0 : d.id) || ""), direction: String((null === d || void 0 === d ? void 0 : d.direction) || ""), request_payload: P(e), response_payload: "", response_status: 0, request_time: r, response_time: Date.now(), duration_ms: Date.now() - r, error_type: "webhook_request_error", error_name: String((null === n || void 0 === n ? void 0 : n.name) || ""), error_message: String((null === n || void 0 === n ? void 0 : n.message) || n || "") }) } } return { ok: !0 } }) }This storage record controls which endpoint receives incoming-message data and which WhatsApp fields are included.
- Location
- chrome.storage.local key 'wa-privacy-business-webhooks-v1'
WhatsApp phone number sent to Alibaba Cloud logging service
We planted a marker phone number in a simulated WhatsApp login and saw it repeatedly leave the device.
On every load it reads and posts the number, with a persistent UUID, to an Alibaba logging endpoint. 60+ POSTs carried the marker.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You have an active, logged-in WhatsApp Web session open in your browser.
The extension reads your WhatsApp phone number from the page and sends it, with a persistent device identifier, to an Alibaba Cloud logging service.
The same request also carries your browser type, extension version, language, and an approximate network location derived from your IP address.
- WhatsApp phone number+15551234567 (illustrative)
Directly identifies your WhatsApp account.
- Extension install UUIDd6ca286c-be29-43d4-be99-f9d69d52e551 (illustrative)
A persistent ID that ties every record from your browser profile together over time.
- Browser and platformChrome 126 on Windows 10
Adds context about the browser and operating system where WhatsApp Web ran.
- Network locationUnited States, 198.51.100.24 (illustrative)
Adds an approximate country and IP-derived location to the record.
- Extension version2.1.5
Records which build of the extension sent the data.
204 No Content -- the logging endpoint acknowledges ingestion without echoing the record back.
- Content-Type
- application/x-protobuf
- x-log-apiversion
- 0.6.0
{"event_source": "extension", "event_type": 100203, "install_time": 1788061608, "platform": "Win32", "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/126.0.0.0", "browser_type": "Chrome", "uuid": "d6ca286c-be29-43d4-be99-f9d69d52e551", "version": "2.1.5", "language": "en-US", "client_ip_country_code": "US", "client_ip_country_name": "United States", "source_plugin": "whatsapp-privacy", "user_phone_num": "+15551234567", "whatsapp_number": "+15551234567", "country_code": "US"} (illustrative -- reconstructed from the observed record shape; a planted marker value was transmitted in the user_phone_num and whatsapp_number fields during dynamic analysis, confirming the data leaves the device)Reading the phone number out of the page, then attaching it to the outbound telemetry record
Deobfuscated content-script handler
deobfuscated/load.jswindow.onload = async function() { try { let e; window.localStorage.getItem("last-wid") ? (e = window.localStorage.getItem("last-wid"), v = e.split("@")[0].substring(1)) : (e = window.localStorage.getItem("last-wid-md"), v = e.split(":")[0].substring(1)), chrome.runtime.sendMessage({ userPhoneNum: v }), chrome.storage.local.set({ userPhoneNum: v }) } catch (e) {}}Deobfuscated background telemetry-assembly function
deobfuscated/background.js function G(t) { let e = t.eventType, n = t.otherParams; const d = U(e, n); d && (n = s(n || {})); const o = Date.now(), a = w.includes(Number(e)); chrome.storage.local.get(null, (function(t) { const s = Date.now() - o, u = L(n); let l = void 0 !== t.browserInfo ? t.browserInfo : "", c = l ? l.userAgent : "", h = l ? l.browser : "", f = void 0 !== t.platform ? t.platform : "", $ = void 0 !== t.installTime ? t.installTime : "", p = t.userPhoneNum, g = (void 0 !== t.firstSendTime && t.firstSendTime, void 0 !== t.uuid ? t.uuid : ""), m = void 0 !== t.version ? t.version : "", E = void 0 !== t.language ? t.language : "", b = void 0 !== t.clientIp ? t.clientIp : "", v = void 0 !== t.clientIpCountryCode ? t.clientIpCountryCode : "", w = void 0 !== t.clientIpCountryName ? t.clientIpCountryName : "", T = { event_source: y, event_type: e, event_name: C(e), event_time: Math.round(new Date / 1e3), install_time: $, platform: f, user_agent: c, browser_type: h, uuid: g, version: m, language: E, client_ip: b, client_ip_country_code: v, client_ip_country_name: w, client_ip_fallback_used: !0 === t.clientIpLookupFallbackUsed ? 1 : 2, ...u, source_plugin: "whatsapp-privacy", ...j(g, p, $, m, !d), ...d ? {} : M(t[A]) }; if (void 0 !== p) { let e = r("c111"), n = e("+" + p).getRegionCode(), i = new e(p.toString(), n), o = i.getRegionCode() ? i.getRegionCode() : "", s = K(o) ? K(o) : ""; d || (T.user_phone_num = p, T.whatsapp_number = p), T.country_code = s, t.phoneNum === p && t.countryCode === s || chrome.storage.local.set({ phoneNum: p, countryCode: s }) } else d || (T.user_phone_num = "", T.whatsapp_number = ""), T.country_code = ""; if (n) for (let e in n) T[e] = n[e]; try { !a && N(e, s, g, p, l) && O(i["a"].TELEMETRY_CONTEXT_LOADED, { event_type_original: e, event_name_original: C(e), has_uuid: g ? 1 : 2, has_phone_num: void 0 !== p && "" !== p ? 1 : 2, has_browser_info: l ? 1 : 2, country_code: T.country_code || "", context_load_duration_ms: s, storage_keys: Object.keys(t || {}).join(",") }), a || k(e, u), S.send(d ? B(T) : T), !a && D(e) && O(i["a"].TELEMETRY_MESSAGE_SENT, { event_type_original: e, event_name_original: C(e), logstore: _.logstore, send_channel: "chrome_runtime_background", ...u }) } catch (x) { a || O(i["a"].TELEMETRY_MESSAGE_FAILED, { event_type_original: e, event_name_original: C(e), error_type: "sls_send_error", error_name: String((null === x || void 0 === x ? void 0 : x.name) || ""), error_message: String((null === x || void 0 === x ? void 0 : x.message) || x || ""), has_chrome_runtime: 1, retry_count: 0 }) } }))- extension-us.us-west-1.log.aliyuncs.com
Alibaba Cloud Simple Log Service (SLS) logstore that receives the phone-number-tagged telemetry record.
Remote config from cdn2.waplus.io gates a promo button in WhatsApp Web
On install, update, and periodically, the extension stores a config fetched from cdn2.waplus.io.
When it has 'Drainage configuration' with wowLink="1", a script injects a button into the group UI, linking to a sibling extension.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-829
- Source
- Dynamic sandbox
The extension periodically fetches a configuration file from cdn2.waplus.io that neither you nor an administrator controls.
When the fetched config sets a specific flag, the extension injects a promotional button linking to a second extension from the same developer.
The behavior of every install can be turned on or off centrally by changing the server's response, without an extension update.
200 OK, JSON body containing a config array; one entry is named 'Drainage configuration' with params.wowLink set to "1".
Fetching the remote config, then acting on its wowLink flag in the page
Deobfuscated config-fetch routine
deobfuscated/background.js function Ct() { chrome.storage.local.get(["zbaseConfig", "configLastFetchTime"], t => { const e = Date.now(), r = Date.now(), n = 3e5; var d; !t.zbaseConfig || !t.configLastFetchTime || r - t.configLastFetchTime > n ? fetch("https://cdn2.waplus.io/extensiton/wa-privacy/config/config.json", { cache: "no-store" }).then(t => t.json()).then(t => { chrome.storage.local.set({ zbaseConfig: t, configLastFetchTime: r }).then(() => { var r; lt(D["a"].REMOTE_CONFIG_LOAD_RESULT, { c_type: 1, entry_source: "background", stage: "fetch_remote_config", config_source: "remote", cache_hit: 2, config_count: Array.isArray(null === t || void 0 === t || null === (r = t.data) || void 0 === r ? void 0 : r.config) ? t.data.config.length : 0, duration_ms: Date.now() - e }) }) }).catch(r => { console.log("fetch config failed", r), t.zbaseConfig && console.log("use cached config"), lt(D["a"].REMOTE_CONFIG_LOAD_RESULT, { c_type: 2, entry_source: "background", stage: "fetch_remote_config", config_source: t.zbaseConfig ? "cached_fallback" : "remote", cache_hit: t.zbaseConfig ? 1 : 2, fallback_used: t.zbaseConfig ? 1 : 2, duration_ms: Date.now() - e, error_type: "remote_config_fetch_failed", ...ct(r) }) }) : (console.log("use cached config, seconds since last fetch:", Math.round((r - t.configLastFetchTime) / 1e3), "seconds"), lt(D["a"].REMOTE_CONFIG_LOAD_RESULT, { c_type: 1, entry_source: "background", stage: "read_cached_config", config_source: "cache", cache_hit: 1, config_age_seconds: Math.round((r - t.configLastFetchTime) / 1e3), config_count: Array.isArray(null === (d = t.zbaseConfig) || void 0 === d || null === (d = d.data) || void 0 === d ? void 0 : d.config) ? t.zbaseConfig.data.config.length : 0, duration_ms: Date.now() - e })) }) }Deobfuscated config-driven button injection
deobfuscated/drainage-module.js "use strict"; "use strict"; o.r(t); var r = o("b109"); async function a() { let e = document.getElementsByClassName(r["e"])[0], n = await chrome.storage.local.get(["zbaseConfig"]); if (e && n.zbaseConfig && n.zbaseConfig.data.config) { const e = document.createElement("style"); e.type = "text/css", e.rel = "stylesheet", document.head.appendChild(e), e.appendChild(document.createTextNode("#wow-community-add-button,#wow-group-add-button{position: relative;z-index: 33; background: rgb(8 196 29);color: white;padding: 5px 7px;font-size: 14px;border-radius: 5px;cursor: pointer; display: flex;justify-content: center; align-items: center; }")), n.zbaseConfig.data.config.forEach(e => { "Drainage configuration" === e.name && e.params && "1" === e.params.wowLink && i() }) } else setTimeout(() => { a() }, 2e3) } function i() { let n = document.getElementsByTagName("body")[0]; n.addEventListener("click", (function() { setTimeout((function() { chrome.storage.local.get(["wow_group_add_button_noshow"], (function(n) { if (!n.wow_group_add_button_noshow && !document.getElementById("wow-group-add-button")) { let n = document.getElementsByClassName(r["f"])[0], t = ""; if (n && (t = n.getElementsByClassName(r["g"])[0]), n && !t) { let t = document.createElement("p"), o = document.createElement("img"); o.src = chrome.runtime.getURL("icons/wowLogo.png"), o.style.width = "20px", t.appendChild(o), t.innerHTML += "WA Group Sender <span style='padding: 0 2px 0 10px; font-size: 13px;'>X</span>", t.id = "wow-group-add-button", n.appendChild(t); let a = n.getElementsByClassName(r["h"])[0].getElementsByTagName("h1")[0].innerHTML; t.onclick = function() { window.open("https://chrome.google.com/webstore/detail/wa-group-sender/kilbeicibedchlamahiimkjeilnkgmeo?utm_source=privacy_wow_buttonInstall_" + a) }, document.getElementById("wow-group-add-button").getElementsByTagName("span")[0].onclick = function() { n.removeChild(t), chrome.storage.local.set({ wow_group_add_button_noshow: !0 }), window.event ? window.event.cancelBubble = !0 : e.preventDefault() } } } })) }), 500) })) } a() },- cdn2.waplus.io
Serves the remote configuration file that turns the in-page promotional button on or off.
- chrome.google.com
Store listing for a second extension (ID kilbeicibedchlamahiimkjeilnkgmeo), same developer, opened via the injected button with the current group name as a tracking parameter.
+5 more findings not shown