Is WA Contacts Extractor safe?
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.
Who publishes itExport Scraper - 2 other listings from the same operator, 1 of them carrying a finding
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.
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.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
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.
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 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.
| Field | Value | Why 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. |
The popup starts the WhatsApp export and the content script forwards it into the page
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
})
})
}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: () => {}
}The injected page script builds contact rows with names, labels, and last-message fields
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
}
});
}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');
};The download screen writes the collected rows into local export files
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")
}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)
}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