Is WA Contacts Extractor safe?

Medium risk

WA Contacts Extractor is medium risk. Starting a WA Contacts Extractor export asks WhatsApp Web for contacts, chats, groups, labels, or country-filtered contacts, and builds rows with phone numbers, country data, names, business/block status, labels, and last-message details.

Export Scraperv2.1.42Chrome Web Store
45Risk
Who publishes it

Export Scraper - 2 other listings from the same operator, 1 of them carrying a finding

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
Export Scraper

Same store account

2 other listings published from this account, 12k+ users between them. 1 of them carries a finding.

Shared hosts - 1 hostname

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.

service.igexporttools.com
Also called by 8 other listings, including Export Followers free

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

WhatsApp Contact Export Reads Names, Numbers, and Messages

Starting a WA Contacts Extractor export asks WhatsApp Web for contacts, chats, groups, labels, or country-filtered contacts, and builds rows with phone numbers, country data, names, business/block status, labels, and last-message details.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You start an export from the extension popup while WhatsApp Web is open.

The popup supports contacts, chats, groups, labels, and country-filtered exports.

The extension did this

The extension reads WhatsApp contact records from the page and prepares them for a local file download.

The same flow can include last-message text and timestamps for each contact when WhatsApp Web exposes them.

02EvidenceFIELD TABLE
Fields constructed for each exported WhatsApp contact
FieldValueWhy it matters
Phone number
+14155550133 (illustrative)Lets a contact be identified and reached outside WhatsApp.
Country
+1 / United States (illustrative)Adds geographic context to the contact list and can group people by phone-number region.
Saved and public names
Saved: Alex Rivera; public: Alex R. (illustrative)Links the phone number to the name you saved and the public name WhatsApp exposes.
Labels
VIP Customer, Follow Up (illustrative)Preserves any WhatsApp labels associated with the contact, which can reveal how you organize people or leads.
Business and block status
is_business: true; is_blocked: false (illustrative)Shows whether the contact is marked as a business and whether the contact is blocked.
Last message details
Incoming, Tue Mar 26 2024 10:18:04 GMT+0000, YES (illustrative)Can expose the most recent message text, when it was sent, whether it was incoming or outgoing, and unread status.
03EvidenceCODE COMPARE
The code that does this

The popup starts the WhatsApp export and the content script forwards it into the page

What it actually does
Readable popup export triggerpopup.49fbeb31.js
async function Ke() {
          let me = {
            filterType: ae.value
          };
          if (ae.value === "contacts") me.value = pe.value;
          else if (ae.value === "chats") me.value = ue.value;
          else if (ae.value === "groups") {
            if (!J.value || J.value.length <= 0) {
              v.default.warn("Please select a group.");
              return
            }
            me.value = J.value, me.includeAdmin = Q.value
          } else if (ae.value === "labels") {
            if (!ke.value) {
              v.default.warn("Please select a label.");
              return
            }
            me.value = ke.value
          } else if (ae.value === "countries") {
            if (console.log("countryCode", ut.value), !ut.value) {
              v.default.warn("Please select a country.");
              return
            }
            me.value = ut.value
          } else {
            v.default.warn("Not Filter Type");
            return
          }
          if (ee.GLOBAL_STORE.workNum >= 3 && await (0, R.openCommentPage)() === "Ok") {
            (0, W.sendToBackground)("LOGS", {
              title: "Open Comment Page",
              content: ee.GLOBAL_STORE.workNum
            });
            return
          }
          console.log("请求参数params==》", me), (await (0, W.sendToCS)("FOR-P-C---", {
            command: "_wa_contacts_24uvb___WA_CONTACTS__",
            params: me
          }, {
            url: "https://web.whatsapp.com/*"
          })).status && ((0, ee.PATCH)({
            downloadingShow: !0
          }), ee.GLOBAL_STORE.countWork()), (0, W.sendToBackground)("LOGS", {
            title: "Start Exporting",
            content: JSON.stringify({
              filterType: ae.value,
              params: me.value
            })
          })
        }
Readable content-script bridge methodsplasmo-overlay.db6b23e9.js
return {
          forwardCommandFromPopup: () => {
            (0, I.addListener)("FOR-P-C---", async (te, Ie) => {
              let Le = {
                status: !0
              };
              if (te.command === "_wa_contacts_24uvb_____WA_CHECK_NUMBER____") {
                if (fe.value === "checking") {
                  console.warn("busy status..", N.value), Le.status = !1, Le.checkingTaskId = N.value, Ie(Le);
                  return
                }
                let A = M.GLOBAL_STORE.getTask(te.taskId);
                if (!A) console.warn("no task", te.taskId);
                else {
                  fe.value = "checking", N.value = te.taskId, Ie(Le);
                  for (let me = 0; me < A.list.length && fe.value === "checking"; me++) {
                    let Ae = A.list[me];
                    window.postMessage({
                      k: te.command,
                      n: Ae.phoneNumber,
                      i: te.taskId
                    }), await (0, Z.sleep)(500)
                  }
                  fe.value !== "idle" && (fe.value = "idle", N.value = "");
                  return
                }
              } else if (te.command === "_wa_contacts_24uvb___WA_READY_STATUS__") {
                const A = chrome.runtime.getURL("assets/w.js");
                if (document.querySelector(`script[src="${A}"]`)) window.postMessage({
                  k: te.command,
                  params: te.params || {}
                });
                else {
                  let Ae;
                  for (let _e = 0; _e < k.length && (Ae = document.querySelector(k[_e]), !Ae); _e++);
                  Ae ? await (0, H.sendToBackground)("CHANGE-WA-READY-STATE", {
                    state: "not_logged_in"
                  }) : await (0, H.sendToBackground)("CHANGE-WA-READY-STATE", {
                    state: "connecting"
                  })
                }
              } else if (te.command === "__BE_BUSY__") {
                fe.value === "checking" && (console.warn("busy status..", N.value), Le.status = !1, Le.checkingTaskId = N.value), Ie(Le);
                return
              } else if (te.command === "__STOP_TASK__") fe.value !== "idle" && (fe.value = "idle", N.value = "");
              else if (te.command === "_wa_contacts_24uvb___WA_CONTACTS__") {
                (0, M.PATCH)({
                  downloadTask: {
                    state: "downloading",
                    fileList: []
                  }
                }), Ie(Le), await (0, Z.sleep)(1e3), window.postMessage({
                  k: te.command,
                  params: te.params || {}
                });
                return
              } else window.postMessage({
                k: te.command,
                params: te.params || {}
              });
              Ie(Le)
            })
          },
          addListenerFromInject: () => {
            window.addEventListener("message", async function(te) {
              switch (te.data.k) {
                case "_wa_contacts_24uvb___WA_READY_STATUS_RESP__":
                  M.GLOBAL_STORE.waReadyState !== te.data.ret && await (0, H.sendToBackground)("CHANGE-WA-READY-STATE", {
                    state: te.data.ret
                  });
                  break;
                case "_wa_contacts_24uvb___WA_GROUP_LABEL_RESP__":
                  console.log("获取到反馈", te.data), te.data && te.data.ret && (0, M.PATCH)({
                    groupList: te.data.ret.groups || [],
                    labelList: te.data.ret.labels || []
                  });
                  break;
                case "_wa_contacts_24uvb___WA_CHECK_NUMBER_RESP__":
                  if (!te.data.req || !te.data.req.i) {
                    console.warn("no task id", te.data.req);
                    return
                  }
                  M.GLOBAL_STORE.changeNumberExist(te.data.req.i, te.data.req.n, te.data.ret ? "yes" : "no");
                  break;
                case "__WA_CONTACTS_RESP__":
                  let Le = te.data.ret;
                  console.log("ret", Le), Le && (console.log("进来了嘛ret", Le), (0, M.PATCH)({
                    downloadTask: {
                      state: Le.state,
                      fileList: [...M.GLOBAL_STORE.downloadTask.fileList, {
                        title: Le.title,
                        list: Le.list
                      }]
                    }
                  }));
                  break;
                case "_wa_contacts_24uvb__EXCEPTION":
                  te.data && te.data.err && (0, H.sendToBackground)("LOGS", {
                    title: te.data.err,
                    content: te.data.msg,
                    type: "error"
                  });
                  break
              }
            })
          },
          postMeMsg: () => {}
        }
04EvidenceCODE COMPARE
The code that does this

The injected page script builds contact rows with names, labels, and last-message fields

What it actually does
Readable contact-row builderassets/i.js
async function paresAndPostUserData(C, m, i = 'idle') {
    const dataArr = [];
    for (const f of m) {
      if (f['isMe']) continue;
      let user = f['id']['server'] === 'c.us' && f['id']['user'] || f['phoneNumber'] && f['phoneNumber']['server'] === 'c.us' && f['phoneNumber']['user'] || ((() => {
        return console['error']('not Found c.us', f['id']), '';
      })());
      if (!user) continue;
      let formattedPhone;
      try {
        formattedPhone = f['formattedPhone'];
      } catch (A) {}
      let countryCode = '';
      formattedPhone && (countryCode = getAreaCode(formattedPhone));
      !countryCode && user && (countryCode = inferCountryCodeFromPhone('+' + user));
      !countryCode && (countryCode = r(user));
      let E = '',
        K = '';
      if (f['notifyName']) E = f['notifyName'];
      else {
        if (f['verifiedName']) E = f['verifiedName'];
        else {
          if (f['name']) E = f['name'];
          else {
            if (f['mentionName']) E = f['mentionName'];
            else f['formattedName'] && (E = f['formattedName']);
          }
        }
      }
      if (f['verifiedName']) K = f['verifiedName'];
      else {
        if (f['formattedName']) K = f['formattedName'];
        else {
          if (f['notifyName']) K = f['notifyName'];
          else {
            if (f['name']) K = f['name'];
            else f['mentionName'] && (K = f['mentionName']);
          }
        }
      }!countryCode && (countryCode = inferCountryFromName(E, K));
      let data = {
        'country_code': countryCode ? '+' + countryCode : '',
        'country_name': countryCode ? COUNTRY_INFO[countryCode] || '' : '',
        'phone_number': '+' + user,
        'formatted_phone': formattedPhone ? formattedPhone : '+' + user,
        'is_my_contact': f['isMyContact'],
        'saved_name': E || '',
        'public_name': K || '',
        'is_business': f['isBusiness'],
        'is_blocked': f['isContactBlocked'],
        'labels': await paresLabelsStr(f['labels'] || [])
      };
      await addLastMsg(user, data), dataArr['push'](data);
    }
    window['postMessage']({
      'k': '__WA_CONTACTS_RESP__',
      'ret': {
        'state': i,
        'title': C,
        'list': dataArr
      }
    });
  }
Readable last-message readerassets/i.js
addLastMsg = async (C, m) => {
      m['last_msg_text'] = '', m['last_msg_date'] = '', m['last_msg_type'] = '', m['last_msg_status'] = '';
      const i = await getWPP();
      if (!i) return;
      const f = await i['chat']['get'](C);
      if (!f) return;
      const e = f['msgs']['last']();
      e && e['body'] && (m['last_msg_text'] = e['body'], m['last_msg_date'] = new Date(0x3e8 * e['t'])['toString'](), m['last_msg_type'] = e['id']['fromMe'] ? 'Outgoing' : 'Incoming', m['last_msg_status'] = f['msgs']['unreadCount'] && f['msgs']['unreadCount'] > 0x0 ? 'YES' : 'NO');
    };
05EvidenceCODE COMPARE
The code that does this

The download screen writes the collected rows into local export files

What it actually does
Readable download component export looppopup.49fbeb31.js
const B = (0, i.ref)("csv"),
          j = () => {
            if (y.GLOBAL_STORE.downloadTask.fileList.length > 0) {
              for (let ae of y.GLOBAL_STORE.downloadTask.fileList) {
                const {
                  title: pe,
                  list: ue
                } = ae;
                if (ue.length > 0) {
                  if (!y.GLOBAL_STORE.isVip && ue.length > 10)
                    for (let Z = 10; Z < ue.length; Z++) {
                      const Y = ue[Z];
                      let Q = Object.keys(Y);
                      for (let J = 0; J < Q.length; J++) {
                        let le = Q[J];
                        le === "country_code" || le === "country_name" || (Y[le] = "***PRO***")
                      }
                    }
                  console.log("下载===>", ue);
                  const ce = "WA-EXTRACTOR-" + pe;
                  B.value === "csv" ? U(ce, ue) : B.value === "excel" ? ee(ce, ue) : B.value === "json" ? R(ce, ue) : B.value === "vCard" && k(ce, ue)
                }
              }(0, _.sendToBackground)("LOGS", {
                title: "DownLoad File",
                content: JSON.stringify({
                  exportFileType: B.value,
                  countNum: z.value
                }),
                type: "debug"
              })
            }
          },
          W = () => {
            (0, _.sendToBackground)("LOGS", {
              title: "Go Back Home",
              content: y.GLOBAL_STORE.workNum,
              type: "debug"
            }), (0, y.PATCH)({
              downloadingShow: !1
            })
          };

        function ee(ae, pe) {
          (0, A.saveJsonToExcel)(pe, ae)
        }

        function U(ae, pe) {
          (0, A.exportCsv)(pe, ae)
        }

        function R(ae, pe) {
          const ue = JSON.stringify(pe);
          (0, A.downloadFile)(ue, ae, ".json")
        }

        function k(ae, pe) {
          let ue = "";
          for (let ce = 0; ce < pe.length; ce++) {
            const Z = pe[ce];
            let Y = new M;
            Y.set("fn", Z.public_name || Z.saved_name), Y.set("tel", Z.phone_number), ue += Y.toString("4.0")
          }
          if (!ue) {
            console.error("str is null, download vcard error.");
            return
          }(0, A.downloadFile)(ue, ae, ".vcf")
        }
Readable browser file writerbackground.5fadff2f.js
function Wc(e, t, r) {
    if (typeof _i < "u" && _i.writeFileSync) return r ? _i.writeFileSync(e, t, r) : _i.writeFileSync(e, t);
    if (typeof Deno < "u") {
      if (r && typeof t == "string") switch (r) {
        case "utf8":
          t = new TextEncoder(r).encode(t);
          break;
        case "binary":
          t = jc(t);
          break;
        default:
          throw new Error("Unsupported encoding " + r)
      }
      return Deno.writeFileSync(e, t)
    }
    var n = r == "utf8" ? la(t) : t;
    if (typeof IE_SaveFile < "u") return IE_SaveFile(n, e);
    if (typeof Blob < "u") {
      var a = new Blob([wS(n)], {
        type: "application/octet-stream"
      });
      if (typeof navigator < "u" && navigator.msSaveBlob) return navigator.msSaveBlob(a, e);
      if (typeof saveAs < "u") return saveAs(a, e);
      if (typeof URL < "u" && typeof document < "u" && document.createElement && URL.createObjectURL) {
        var i = URL.createObjectURL(a);
        if (typeof chrome == "object" && typeof(chrome.downloads || {}).download == "function") return URL.revokeObjectURL && typeof setTimeout < "u" && setTimeout(function() {
          URL.revokeObjectURL(i)
        }, 6e4), chrome.downloads.download({
          url: i,
          filename: e,
          saveAs: !0
        });
        var s = document.createElement("a");
        if (s.download != null) return s.download = e, s.href = i, document.body.appendChild(s), s.click(), document.body.removeChild(s), URL.revokeObjectURL && typeof setTimeout < "u" && setTimeout(function() {
          URL.revokeObjectURL(i)
        }, 6e4), i
      }
    }
    if (typeof $ < "u" && typeof File < "u" && typeof Folder < "u") try {
      var o = File(e);
      return o.open("w"), o.encoding = "binary", Array.isArray(t) && (t = xi(t)), o.write(t), o.close(), t
    } catch (c) {
      if (!c.message || !c.message.match(/onstruct/)) throw c
    }
    throw new Error("cannot save file " + e)
  }
06EvidencePLAIN NOTE
Dynamic-analysis caveat

The recorded browser test reached WhatsApp Web but was not logged in, so it did not trigger the export or capture contact arrays in page messages. The available evidence for this claim is the static proof of the export path plus the negative observation that no contact-data request to a third-party endpoint was recorded.

What it can do

Permissions this extension asks for, as declared in version 2.1.38. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to. The current listed version is 2.1.42, which we have not unpacked yet.

  • Read and change your data on whatsapp.com

    *://*.whatsapp.com/*

  • Store data in your browser

    storage

  • Sign you in with your Google account

    identity

  • Store an unlimited amount of data in your browser

    unlimitedStorage

Updated 30 September 2026dcidojkknfgophlmohhpdlmoiegfbkdd