Is Privacy Extension for WhatsApp Privacy safe?

High risk

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.…

privacypublisherv2.1.7Chrome Web Store
75Risk
Who publishes it

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.

Publisher
privacypublisher

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
What actually happens
You did this

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 did this

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.

Fields the webhook can include
  • Message text
    Hi 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 number
    14155550137

    Lets the configured endpoint link the message to the person who sent it.

  • Sender names
    Maya Chen

    Lets the configured endpoint receive names associated with the sender in WhatsApp.

  • Group name
    West Coast Operations

    Lets the configured endpoint see which group conversation the incoming message came from.

  • Message timestamp and type
    2026-07-14T10:18:32Z, chat

    Lets the configured endpoint place each message in time and distinguish chat messages from other message types.

Captured request
GEThttps://webhook-canary-test.requestcatcher.com/<redacted>

No webhook request reached this configured test endpoint during dynamic analysis; WhatsApp login and an incoming message were unavailable.

The code that does this

Incoming-message listener and webhook delivery code

Readable version

Deobfuscated listener that packages incoming WhatsApp messages

inject-scripts/inject-script.js
function 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.js
function 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        }      })    }
What's stored on your device

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
What actually happens
You did this

You have an active, logged-in WhatsApp Web session open in your browser.

The extension did this

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.

Fields present in the outbound Alibaba Cloud record
  • WhatsApp phone number
    +15551234567 (illustrative)

    Directly identifies your WhatsApp account.

  • Extension install UUID
    d6ca286c-be29-43d4-be99-f9d69d52e551 (illustrative)

    A persistent ID that ties every record from your browser profile together over time.

  • Browser and platform
    Chrome 126 on Windows 10

    Adds context about the browser and operating system where WhatsApp Web ran.

  • Network location
    United States, 198.51.100.24 (illustrative)

    Adds an approximate country and IP-derived location to the record.

  • Extension version
    2.1.5

    Records which build of the extension sent the data.

Captured request
POSThttps://extension-us.us-west-1.log.aliyuncs.com/logstores/scrm-privacy/track

204 No Content -- the logging endpoint acknowledges ingestion without echoing the record back.

Headers
Content-Type
application/x-protobuf
x-log-apiversion
0.6.0
Body
{"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)
The code that does this

Reading the phone number out of the page, then attaching it to the outbound telemetry record

Readable version

Deobfuscated content-script handler

deobfuscated/load.js
window.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          })        }      }))
External destination
    • 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
What actually happens
You did this

The extension periodically fetches a configuration file from cdn2.waplus.io that neither you nor an administrator controls.

The extension did this

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.

Captured request
GEThttps://cdn2.waplus.io/extensiton/wa-privacy/config/config.json

200 OK, JSON body containing a config array; one entry is named 'Drainage configuration' with params.wowLink set to "1".

The code that does this

Fetching the remote config, then acting on its wowLink flag in the page

Readable version

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()  },
Destinations involved
    • 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

Updated 30 September 2026cgipcgghboamefelooajpiabilddemlh