Is InTouchApp Phone Contacts & Data Saver safe?
InTouchApp is high risk. On WhatsApp Web, InTouchApp injects a script into WhatsApp's page context rather than the usual isolated sandbox. The script reads WhatsApp's internal data store, and AI Reply sends recent chat messages to InTouchApp's servers for a reply.…
Who publishes itVOLARE TECHNOLOGIES PRIVATE LIMITED - 1 other listing from the same operator, none carrying a finding
VOLARE TECHNOLOGIES PRIVATE LIMITED - 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, 6k+ 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
WhatsApp MAIN-world injection exposes chats, contacts, and messages
On WhatsApp Web, InTouchApp injects a script into WhatsApp's page context rather than the usual isolated sandbox.
The script reads WhatsApp's internal data store, and AI Reply sends recent chat messages to InTouchApp's servers for a reply.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-829
- Source
- Dynamic sandbox
You open WhatsApp Web with InTouchApp installed.
No explicit consent step precedes this injection.
The extension injects a script into WhatsApp's page context (world:'MAIN'), bypassing Chrome's normal isolated-world sandbox.
This gives the injected script direct access to WhatsApp's internal data store, contacts, chats, messages, groups, labels, statuses, and presences, without any user prompt.
Background triggers MAIN-world injection on WhatsApp tab load
Injected script hooks WhatsApp webpack runtime (ita-wa-store-bridge.js:103–154)
// Captures WhatsApp's internal webpack require() functionfunction getWebpackRequire() { const chunkKeys = Object.keys(window).filter(key => key.startsWith('webpackChunk')); for (const key of chunkKeys) { const chunk = window[key]; chunk.push([ ['ita_store_bridge_' + Math.random().toString(36).slice(2)], {}, (req) => { window.require = req; // installs WhatsApp's require() on window } ]); }}// Loads WhatsApp internal store modules via captured require()function ensureStore() { const req = getWebpackRequire(); window.Store = window.Store || {}; window.Store.WidFactory = req('WAWebWidFactory'); window.Store.LidUtils = req('WAWebApiContact'); window.Store.ContactCollection = req('WAWebContactCollection').ContactCollection; // + WAWebChatCollection, WAWebMsgCollection, WAWebGroupMetadataCollection, // WAWebLabelCollection, WAWebStatusCollection, WAWebPresenceCollection}Debug dump helpers installed as global window functions (ita-wa-store-bridge.js:658–676)
// Exposed globally on WhatsApp's page context — accessible to any page scriptwindow.itaWARaidStore = () => raidRequire();window.itaWASnapshotStore = () => snapshotStore(req);window.itaWADumpContacts = (options) => dumpCollection(req, 'contacts', options);window.itaWADumpChats = (options) => dumpCollection(req, 'chats', options);window.itaWADumpMessages = (options) => dumpCollection(req, 'messages', options);window.itaWADumpLabels = (options) => dumpCollection(req, 'labels', options);window.itaWADumpStatuses = (options) => dumpCollection(req, 'statuses', options);window.itaWADumpPresences = (options) => dumpCollection(req, 'presences', options);- Contact details+1 415 555 0123 · "Alice Smith" · isMyContact: true
Phone numbers, display names, push names, picture URLs, and whether each contact is a WhatsApp user, business, or saved contact.
- Chat listAlice Smith · unread: 3 · lastMsg: "Can we talk tomorrow?"
All open chats including contact identity, last message preview, unread count, mute status, and archive state.
- Message content"Let me check with the team and get back to you" · from: Alice · t: 1749288432
Text of recent messages in any open chat, with sender identity and timestamp. AI Reply reads and sends up to 10 messages per invocation.
- Group membership"Project Alpha" · 14 members · owner: +44 7911 123456
Group name, description, owner, creation date, and the full participant list including phone numbers and admin status for every member.
200 OK, {"status":"success","suggestions":[{"text":"Sounds good, looking forward to hearing from you!"}]}
- Accept
- application/json
- Content-Type
- application/json; charset=utf-8
- Authorization
- Basic YXNOUExrZHZDQWVVUldNa3BtOjxhcGlfdG9rZW4+
- X-Requested-With
- XMLHttpRequest
{ "chats": [ { "sender_id": "me", "sender_name": "Me", "time": 1749288400123456, "message": "Can we meet tomorrow?" }, { "sender_id": "external_contact_1", "sender_name": "Alice", "time": 1749288432000000, "message": "Let me check with the team and get back to you" }, { "sender_id": "me", "sender_name": "Me", "time": 1749288460000000, "message": "No rush, just ping me when you know" } ], "instructions": "", "max_suggestions": 1}- api16.intouchapp.com
InTouchApp's primary API server. Receives chat transcripts (up to 10 messages per AI Reply call) with the account token. Operated by Datability Solutions Pvt Ltd, India.
Gmail Save Sends Sender Contact Details
Clicking InTouchApp's save button in Gmail reads the sender name, email address, and profile image URL from the message list item and posts them as a contact record to https://api16.intouchapp.com/api/v1/contacts/insync_single/.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You click the extension's save button while viewing a Gmail sender row.
The extension reads the sender contact details from Gmail and sends a contact record to InTouchApp.
- Sender nameAlex Chen
Identifies the person or mailbox shown as the Gmail message sender.
- Sender email addressalex.chen@example.com
Adds a direct email address from the Gmail page to the saved contact.
- Profile image URLhttps://lh3.googleusercontent.com/a/ACg8ocJ7Example=s96-c
Adds the sender image URL when Gmail exposes one for the message row.
The background code expects a JSON response and resolves success when the response is OK.
- Accept
- application/json
- Content-Type
- application/json; charset=utf-8
- Authorization
- Basic <redacted>
Gmail save path and contact POST
Readable click handler
dist/contentScript.bundle.js }), e("body").off("click", ".gmail-save").on("click", ".gmail-save", function(t) { var o = e(this).parents('[role="listitem"]'), n = angular.element(o).scope(); n.$apply(function() { n.contact.gmailFetch(o) })Readable Gmail DOM extraction
dist/contentScript.bundle.js key: "gmailInit", value: function(t) { var o = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : void 0, n = arguments[2]; try { var a = {}; if (o = o ? o : e('[role="list"]'), !o || !o.length) throw this.exceptionObj.description = "Type: DOM unavailable " + q().format("YYYY-MM-DD:H:mm:ss") + ', DOM: [role="list"], Platform: Gmail', this.exceptionObj.card_log["dom_unavailable_" + q().format("YYYY_MM_DD_H_mm_ss")] = 'DOM: [role="list"], Platform: Gmail', this.exceptionObj; var M = e(o).find(".gD")[0], c = e(o).find(".ajn")[0]; a.given = e(M).attr("name"), a.useremail = e(M).attr("email"); var p = e(c).attr("src"); p.includes("default-user") ? "" : a.profilePic = p.replace(/=s.*$/, ""), this.addContactModule.globalButtonVar = e(".nH.g.id"), this.setContactForm(this.addContactModule.globalButtonVar, t, void 0, a, function() { n(a) }) } catch (e) { e.description || (e.description = "Type: JS Crash " + q().format("YYYY-MM-DD:H:mm:ss") + ", Function: gmailInit (service.js), Platform: Gmail", e.isFatal = !0, e.card_log["js_crash_" + q().format("YYYY_MM_DD_H_mm_ss")] = "Function: gmailInit (service.js), Platform: gmail"), this.reportException(e.description, e.isFatal, e.card_log) } }Readable contact message
dist/contentScript.bundle.js key: "addContactV2", value: function() { var t = this; e(".intouch-alert.server-error").addClass("ng-hide"), chrome.runtime.sendMessage(JSON.stringify({ api: "add_contact_v2", contact: this.contactObj, source: this.utm_source, term: this.utm_term }), function(o) { if (o.success) { try { t.contactObj.icontactId = o.data.icontact_id } catch (e) {} t.addContactCallback() } else t.form.addContactForm.$submitted = !1, t.contactError = o.error, t.$scope.$apply(), e(".intouch-alert.server-error").removeClass("ng-hide") }) }Readable background POST
dist/background.bundle.js function C(e, t, r) { return new Promise(function(n) { var o = Z.getContactObject(e), a = {}; a.data = o[0], a.source = t, a.term = r, chrome.storage.local.get("api_token", function(e) { if (chrome.runtime.lastError) return console.error("Error retrieving stored data:", chrome.runtime.lastError.message), void n(chrome.runtime.lastError.message); var t = G.default.CONSUMER_KEY + ":" + e.api_token; fetch(G.default.APP_SERVER_URL + "/v1/contacts/insync_single/", { method: "POST", signal: AbortSignal.timeout(G.default.TIMEOUT), headers: { "Content-Type": "application/json; charset=utf-8", Accept: "application/json", Authorization: "Basic " + btoa(t) }, body: JSON.stringify(a) }).then(function(e) { if (!e.ok) throw { status: e.status, statusText: e.statusText }; return e.json() }).then(function(e) { n({ success: !0, data: e }) }).catch(function(e) { console.error("API calling failed:", e); var t = Z.setErrorMessage(e); n({ success: !1, error: t }) }) }) }) }Contact-object conversion before upload
key: "getJCFFormat", value: function(e) { var t = this.fetchName(e.given), r = [{ name: { prefix: t.prefix, given: t.given, middle: t.middle, family: t.family, suffix: t.suffix }, avatars: [{ name: { prefix: t.prefix, given: t.given, middle: t.middle, family: t.family, suffix: t.suffix }, phone: [{ type: "mobile", number: e.ContactMobile, label: "Mobile" }] }], context: e.context, context_emoji: e.emoji ? e.emoji.selectedEmoji : "", type: "person", dirty: !0, deleted: !1, read_only: !1 }]; return r } }, { key: "getContactObject", value: function(e) { var t = this.getJCFFormat(e); return t = this.getEmail(e.useremail, t), t = this.getOrganization(e, t), t = this.getNotes(e.note, t), t = this.addPreformattedElements(e, t), t = this.getProfilePic(e.profilePic, t), t = this.getTagObj(e.selectedList, t) } }, { key: "updateContactObject", value: function(e) { var t = this.fetchName(e.given), r = [e.icontact]; return r[0] && r[0].avatars && r[0].avatars[0] && (r[0].name = t, r[0].avatars[0].name = t, r[0].context = e.context, r[0].context_emoji = e.emoji ? e.emoji.selectedEmoji : "", r = this.getPhone(e.ContactMobile, r), r = this.getEmail(e.useremail, r), r = this.getOrganization(e, r), r = this.getNotes(e.note, r), r = this.addPreformattedElements(e, r), r = this.getProfilePic(e.profilePic, r), r = this.getTagObj(e.selectedList, r)), r } }, { key: "getPhone", value: function(e, t) { return e = e.toString(), e && (t[0].avatars[0].phone && t[0].avatars[0].phone.length ? t[0].avatars[0].phone[0].number = e : (e = [{ number: e, label: "Personal" }], t[0].avatars[0].phone = e)), t } }, { key: "getEmail", value: function(e, t) { return null != e && (t[0].avatars[0].email && t[0].avatars[0].email.length ? t[0].avatars[0].email[0].address = e : (e = [{ address: e, label: "Personal" }], t[0].avatars[0].email = e)), t } }, { key: "getOrganization", value: function(e, t) { return null == e.jobTitle && null == e.company || (t[0].avatars[0].organization && t[0].avatars[0].organization.length ? (t[0].avatars[0].organization[0].position = e.jobTitle, t[0].avatars[0].organization[0].company = e.company) : (e = [{ position: e.jobTitle, company: e.company }], t[0].avatars[0].organization = e)), t } }, { key: "getNotes", value: function(e, t) { return null != e && (t[0].avatars[0].notes && t[0].avatars[0].notes.length ? t[0].avatars[0].notes[0].text = e : (e = [{ text: e }], t[0].avatars[0].notes = e)), t } }, { key: "getProfilePic", value: function(e, t) { return e && "undefined" != e && (t[0].avatars[0].photo && t[0].avatars[0].photo.length ? t[0].avatars[0].photo[0].url = e : (e = [{ url: e }], t[0].avatars[0].photo = e)), t } }, { key: "addPreformattedElements", value: function(e, t) { return e.social && (t[0].avatars[0].social = e.social), e.website && (t[0].avatars[0].website = e.website), e.address && (t[0].avatars[0].address = e.address), t }- api16.intouchapp.com
Receives the contact record created from the Gmail save action.
LinkedIn Save Sends Profile Contact Details
Clicking InTouchApp's save on a LinkedIn profile posts page-read details to https://api16.intouchapp.com/api/v1/contacts/insync_single/: name, phone, email, job title, company, photo, location, website, social links when present.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You click the extension's save button on a LinkedIn profile.
The extension reads the LinkedIn profile fields and sends a contact record to InTouchApp.
- Profile nameMaya Rao
Identifies the LinkedIn member whose profile you saved.
- Phone number+1 415 555 0148
Adds a direct contact number from the profile contact panel when LinkedIn exposes one.
- Email addressmaya.rao@example.com
Adds an email address from the profile contact panel when one is visible.
- Job and companyProduct Lead at Example Systems
Ties the saved contact to a current role and employer shown on the profile.
- Social and website linkslinkedin.com/in/mayarao, @mayarao
Adds profile-linked identifiers such as LinkedIn, Twitter, Skype, and website fields when present.
- Location and photoSan Francisco, https://media.licdn.com/dms/image/C4D03AQHExample/profile-displayphoto-shrink_400_400/0/1700000000000
Adds context from the profile location and profile image URL.
The background code expects a JSON response and resolves success when the response is OK.
- Accept
- application/json
- Content-Type
- application/json; charset=utf-8
- Authorization
- Basic <redacted>
LinkedIn save path and contact POST
Readable click handler
dist/contentScript.bundle.js }), e("body").off("click", ".linkedin-save").on("click", ".linkedin-save", function() { var t = e(this).parent().parent().parent(), o = angular.element(e("body")).scope(); o.$apply(function() { o.contact.linkedinFetch(t) })Readable LinkedIn DOM extraction
dist/contentScript.bundle.js key: "linkedInit", value: function(t, o, n) { try { var a = {}, M = e(o).find("#pv-contact-info")[0].innerText.trim(), c = "", p = ""; try { c = e(o).find(".pv-contact-info__contact-type.ci-phone").find(".pv-contact-info__ci-container").children()[0].innerText.trim(), p = e(o).find(".pv-contact-info__contact-type.ci-phone").find(".pv-contact-info__ci-container").children()[1].innerText.trim() } catch (e) {} var b = ""; try { b = e(o).find(".pv-contact-info__contact-type.ci-email").find(".pv-contact-info__contact-link")[0].innerText.trim() } catch (e) {} var i = ""; try { i = e("body").children(".ember-view").find(".pv-top-card-section__photo").css("background-image").split('url("')[1].split('")')[0] } catch (e) {} var r = ""; try { r = e(e(e(e("#experience-section").find(".section-info").children()[0]).find(".pv-entity__summary-info")[0]).children()[1]).find(".pv-entity__secondary-title")[0].innerText.trim() } catch (e) {} var z = ""; try { z = e(e(e("#experience-section").find(".section-info").children()[0]).find(".pv-entity__summary-info")[0]).children()[0].innerText.trim() } catch (e) {} var s = ""; try { s = e(".pv-top-card-section__location")[0].innerText.trim(); var d = s.split(",")[0].trim(); if ("" != d) { a.address = new Array; var O = {}; O.label = "Office", O.city = d, a.address.push(O) } } catch (e) {} var u = "", l = ""; try { var A = e(o).find(".pv-contact-info__contact-type.ci-websites").find(".pv-contact-info__contact-link")[0]; u = A.innerText.trim(); try { l = e(A).siblings()[0].innerText.trim().replace(/[()]/g, "") } catch (e) {} } catch (e) {} try { if ("" != u) { a.website = new Array; var f = {}; f.label = l, f.url = u, a.website.push(f) } } catch (e) {} var m = ""; try { m = e(o).find(".pv-contact-info__contact-type.ci-birthday > .pv-contact-info__ci-container")[0].innerText.trim() } catch (e) {} var h = ""; try { h = e(o).find(".pv-contact-info__contact-type.ci-twitter").find(".pv-contact-info__contact-link")[0].innerText } catch (e) {} a.given = M, a.ContactMobile = c, a.useremail = b, a.profilePic = i, a.jobTitle = z, a.company = r, a.addFrom = "li", a.social = [{ network_id: window.location.pathname.split("/")[2], network: "li", label: "LinkedIn" }]; try { if (h.length) { var W = { network_id: e.trim(h), network: "tw", label: "Twitter" }; a.social.push(W) } } catch (e) {} try { e(o).find(".pv-contact-info__contact-type.ci-ims").each(function(t, o) { try { var n = e(o).find(".pv-contact-info__ci-container").children()[0].innerText.trim(), M = e(o).find(".pv-contact-info__ci-container").children()[1].innerText.trim().replace(/[()]/g, ""), c = {}; 0 == M.localeCompare("skype", void 0, { sensitivity: "base" }) && (c.network = "sk", c.network_id = n, c.label = M, a.social.push(c)) } catch (e) {} }) } catch (e) {} this.addContactModule.globalButtonVar = e(o), this.setContactForm(this.addContactModule.globalButtonVar, t, void 0, a, function() { n(a) }) } catch (e) { e.description || (e.description = "Type: JS Crash " + q().format("YYYY-MM-DD:H:mm:ss") + ", Function: linkedInit (service.js), Platform: Linkedin", e.isFatal = !0, e.card_log["js_crash_" + q().format("YYYY_MM_DD_H_mm_ss")] = "Function: linkedInit (service.js), Platform: Linkedin"), this.reportException(e.description, e.isFatal, e.card_log) } }Readable contact message
dist/contentScript.bundle.js key: "addContactV2WithPhoto", value: function() { var t = this, o = this.grabContactDetails.selectedContactNumber; this.contactObj.selectedList = this.selectedTags(this.grabContactDetails.tagListInForm); var n = !!this.contactObj.icontact_id, a = this.contactObj.icontact_id ? this.contactObj.icontact_id : null; e(".intouch-alert.server-error").addClass("ng-hide"), this.showAppropriateSingleSavedMessage(); var M = this.contactObj.profilePic; this.contactObj.profilePic = null, chrome.runtime.sendMessage(JSON.stringify({ api: this.contactObj.icontact_id ? "update_contact_v2" : "add_contact_v2", contact: this.contactObj, source: this.utm_source, term: this.utm_term }), function(c) { if (c.success) try { "error" !== c.status ? (t.creditsExpire = !1, n ? t.grabContactDetails.updateCachedContacts(o, t.grabContactDetails.setContactObjInstance(c.data)) : (t.customService.updateCreditBalanceInStorage(-c.count || -1), t.getAccountInfo()), M ? t.getImageFromUrl(M, function(e) { e ? (e.imgUrl = M, t.fetchedList = [{ ContactMobile: o ? o : t.contactObj.ContactMobile, profilePic: e, selected: !0, icontactId: a ? a : c.data.results[0].icontact_id }], t.syncSingleContact(0, event, !0, function() { t.addContactCallback() })) : t.addContactCallback() }) : t.addContactCallback(), t.addContactCallback()) : "error" === c.status && "credits_over" === c.error_code && (t.form.addContactForm.$submitted = !1, t.creditsExpire = !0, t.$scope.$apply()) } catch (e) { console.error("addContactV2WithPhoto:crashed: " + e) } else t.form.addContactForm.$submitted = !1, t.contactError = c.error, t.$scope.$apply(), e(".intouch-alert.server-error").removeClass("ng-hide") }) }Readable background POST
dist/background.bundle.js function C(e, t, r) { return new Promise(function(n) { var o = Z.getContactObject(e), a = {}; a.data = o[0], a.source = t, a.term = r, chrome.storage.local.get("api_token", function(e) { if (chrome.runtime.lastError) return console.error("Error retrieving stored data:", chrome.runtime.lastError.message), void n(chrome.runtime.lastError.message); var t = G.default.CONSUMER_KEY + ":" + e.api_token; fetch(G.default.APP_SERVER_URL + "/v1/contacts/insync_single/", { method: "POST", signal: AbortSignal.timeout(G.default.TIMEOUT), headers: { "Content-Type": "application/json; charset=utf-8", Accept: "application/json", Authorization: "Basic " + btoa(t) }, body: JSON.stringify(a) }).then(function(e) { if (!e.ok) throw { status: e.status, statusText: e.statusText }; return e.json() }).then(function(e) { n({ success: !0, data: e }) }).catch(function(e) { console.error("API calling failed:", e); var t = Z.setErrorMessage(e); n({ success: !1, error: t }) }) }) }) }Contact-object conversion before upload
key: "getJCFFormat", value: function(e) { var t = this.fetchName(e.given), r = [{ name: { prefix: t.prefix, given: t.given, middle: t.middle, family: t.family, suffix: t.suffix }, avatars: [{ name: { prefix: t.prefix, given: t.given, middle: t.middle, family: t.family, suffix: t.suffix }, phone: [{ type: "mobile", number: e.ContactMobile, label: "Mobile" }] }], context: e.context, context_emoji: e.emoji ? e.emoji.selectedEmoji : "", type: "person", dirty: !0, deleted: !1, read_only: !1 }]; return r } }, { key: "getContactObject", value: function(e) { var t = this.getJCFFormat(e); return t = this.getEmail(e.useremail, t), t = this.getOrganization(e, t), t = this.getNotes(e.note, t), t = this.addPreformattedElements(e, t), t = this.getProfilePic(e.profilePic, t), t = this.getTagObj(e.selectedList, t) } }, { key: "updateContactObject", value: function(e) { var t = this.fetchName(e.given), r = [e.icontact]; return r[0] && r[0].avatars && r[0].avatars[0] && (r[0].name = t, r[0].avatars[0].name = t, r[0].context = e.context, r[0].context_emoji = e.emoji ? e.emoji.selectedEmoji : "", r = this.getPhone(e.ContactMobile, r), r = this.getEmail(e.useremail, r), r = this.getOrganization(e, r), r = this.getNotes(e.note, r), r = this.addPreformattedElements(e, r), r = this.getProfilePic(e.profilePic, r), r = this.getTagObj(e.selectedList, r)), r } }, { key: "getPhone", value: function(e, t) { return e = e.toString(), e && (t[0].avatars[0].phone && t[0].avatars[0].phone.length ? t[0].avatars[0].phone[0].number = e : (e = [{ number: e, label: "Personal" }], t[0].avatars[0].phone = e)), t } }, { key: "getEmail", value: function(e, t) { return null != e && (t[0].avatars[0].email && t[0].avatars[0].email.length ? t[0].avatars[0].email[0].address = e : (e = [{ address: e, label: "Personal" }], t[0].avatars[0].email = e)), t } }, { key: "getOrganization", value: function(e, t) { return null == e.jobTitle && null == e.company || (t[0].avatars[0].organization && t[0].avatars[0].organization.length ? (t[0].avatars[0].organization[0].position = e.jobTitle, t[0].avatars[0].organization[0].company = e.company) : (e = [{ position: e.jobTitle, company: e.company }], t[0].avatars[0].organization = e)), t } }, { key: "getNotes", value: function(e, t) { return null != e && (t[0].avatars[0].notes && t[0].avatars[0].notes.length ? t[0].avatars[0].notes[0].text = e : (e = [{ text: e }], t[0].avatars[0].notes = e)), t } }, { key: "getProfilePic", value: function(e, t) { return e && "undefined" != e && (t[0].avatars[0].photo && t[0].avatars[0].photo.length ? t[0].avatars[0].photo[0].url = e : (e = [{ url: e }], t[0].avatars[0].photo = e)), t } }, { key: "addPreformattedElements", value: function(e, t) { return e.social && (t[0].avatars[0].social = e.social), e.website && (t[0].avatars[0].website = e.website), e.address && (t[0].avatars[0].address = e.address), t }- api16.intouchapp.com
Receives the contact record created from the LinkedIn profile save action.