Is WaSendPlus - WhatsApp CRM & ChatGPT safe?

High risk

WaSendPlus is high risk. After license validation, WaSendPlus posts a machine registration record to Keygen built from the WhatsApp phone number, popup device name, OS family, and browser version. Not reached live; test license rejected. Evidenced from source.…

wasenderplusv0.3.14Chrome Web Store
78Risk
Who publishes it

wasenderplus - 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
wasenderplus

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

Publishers can request a review.

Findings

Device registration sends phone and system details

After license validation, WaSendPlus posts a machine registration record to Keygen built from the WhatsApp phone number, popup device name, OS family, and browser version.

Not reached live; test license rejected.

Evidenced from source.

Severity
High unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You activate WaSendPlus with a license key and device name.

The extension did this

The extension prepares a machine registration record that links your WhatsApp phone number to that device.

Machine registration fields built by the popup code
  • WhatsApp phone number
    +1 555 010 0199 (illustrative)

    This ties the registration record back to the phone number stored by the extension.

  • Device name
    Work laptop (illustrative)

    This is the name you typed for the device during activation.

  • Operating system
    Windows 10 (illustrative)

    This describes the kind of computer or operating system where you activated the extension.

  • Browser details
    Chrome 126.0.6478.127 (illustrative)

    This records the browser family and version used for the activation.

  • License credential
    Authorization: License WASEND-2026-ABCD (illustrative)

    The registration request authorizes itself with the license key returned by the validation response.

Captured request
POSThttps://api.keygen.sh/v1/accounts/telegramrocket-80ee1bb5/machines
Headers
Accept
application/json
Content-Type
application/json
Authorization
License <redacted>
The code that does this

Activation branch that posts the machine record

Readable version

Formatted popup activation function

popup/popup.js
function y() {    var n = document.getElementById("activateLicenseKey").value,      r = document.getElementById("activateDeviceName").value;    if (n.length < 4 && r.length < 4) return alert("Please name this device more than 4 words ."), void l("Activate", e, a, t);    chrome.storage.sync.get({      phoneNumber: ""    }, (async function(d) {      const m = d.phoneNumber;      m ? await w(key = n, fingerprint = m).then((async d => {        const {          meta: u,          data: y,          errors: g        } = d;        if (u.valid) console.log(u.valid), await chrome.storage.sync.set({          licenseKey: n        }), window.location.replace("./options.html?isWeb=true");        else if (null != y) await async function(e, t, n, a, o, i) {          const r = await fetch(`https://${s}/v1/accounts/${c}/machines`, {              method: "POST",              headers: {                Authorization: `License ${e.attributes.key}`,                "Content-Type": "application/json",                Accept: "application/json"              },              body: JSON.stringify({                data: {                  type: "machine",                  attributes: {                    fingerprint: t,                    name: n,                    platform: a,                    metadata: {                      browser: o,                      version: i                    }                  },                  relationships: {                    license: {                      data: {                        type: "license",                        id: e.id                      }                    }                  }                }              })            }),            {              data: l,              errors: d            } = await r.json();          return {            data: l,            errors: d          }        }(y, m, r, i.os.family, i.browser.family, i.browser.version).then((async o => {          const {            data: i,            errors: c          } = o;          if (c) return console.log(c), console.log(c[0].code, c[0].title, c[0].detail), void alert(c[0].detail);          l("Activation Successful !", e, t, a), await chrome.storage.sync.set({            licenseKey: n          }), await chrome.tabs.sendMessage(v.id, {            message: "activationKeySuccess"          }).catch((e => {            console.error(e)          })), window.location.replace("./options.html?isWeb=true")        }));        else if (!u.valid) return console.log(u.code, u.detail), alert(u.detail), void l("Activate", e, o, t)      })) : (u("PLease login to Whatsapp Web before you activate license !"), l("Activate", e, o, t))    }))  }
Destination receiving the activation record
    • api.keygen.sh

    Keygen license-management API receives the machine registration record for the WaSendPlus account telegramrocket-80ee1bb5.

AI reply sends WhatsApp chats to OpenAI

WaSendPlus's AI reply flow in WhatsApp Web reads recent message elements, builds a prompt with the text and sender labels, and sends it to the background script, which posts it to ChatGPT or OpenAI's API.

Not triggered live; not logged in.

Severity
High unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You ask the extension to draft an AI reply inside WhatsApp Web.

The extension did this

The extension reads recent messages from the current chat and turns them into a prompt.

WhatsApp chat data placed into the prompt
  • Recent message text
    1: Are we still meeting at 5? (illustrative)

    This can include private conversation content from the WhatsApp chat you are viewing.

  • Sender labels
    Me: Running five minutes late (illustrative)

    The prompt keeps track of which recent messages were yours and which came from contacts.

  • Message count setting
    aiReplyLastMessage = 10 (illustrative)

    The extension limits how much chat history it reads using a stored setting, defaulting to recent messages when unset.

  • Reply instruction
    As me, give an utterance completing the following chat conversation flow. (illustrative)

    The generated prompt tells the remote service to continue the conversation in your writing style.

Captured request
POSThttps://chat.openai.com/backend-api/conversation
Headers
Content-Type
application/json
Authorization
Bearer <redacted>
Captured request
POSThttps://api.openai.com/v1/chat/completions
Headers
Content-Type
application/json
Authorization
Bearer <redacted>
The code that does this

Chat scraping and forwarding path

Readable version

Formatted chat-history extractor

content/contentScript.js
async function n(n) {    const o = [];    let i = n.querySelectorAll("._amk4._amkd._amk5");    const {      aiReplyLastMessage: s    } = await t(["aiReplyLastMessage"]) || {      aiReplyLastMessage: 10    }, l = new e;    i = Array.from(i).slice(-s), i.forEach((e => {      let t = "";      const n = e.querySelectorAll(".copyable-text");      if (n.length > 0) n.forEach((e => {        const n = e.getAttribute("data-pre-plain-text");        if (null !== n)          if (null !== e.closest(".message-out")) t += "Me: ";          else {            let e = n.replace(/\[.*?\]\s*/, "").slice(0, -2);            l.add(e);            const a = l.getPosition(e) + 1;            t += a + ": "          }        else {          const n = a(e);          void 0 !== n && (t += n)        }      }));      else if (e.classList.contains("ATT")) {        const n = e.parentNode.previousElementSibling.getAttribute("aria-label");        if (null !== n)          if (null !== e.closest(".message-out")) {            t += "Me: ";            const n = a(e);            void 0 !== n && (t += n)          } else {            let o = n.replace(/\[.*?\]\s*/, "").slice(0, -1);            l.add(o);            const i = l.getPosition(o) + 1;            t += i + ": ";            const s = a(e);            void 0 !== s && (t += s)          }      }      0 !== t.length && o.push(t)    })), console.log(o);    const r = o[o.length - 1];    let c = !1;    return r?.includes("Me:") && (c = !0), {      chatHistoryShort: o.join("\n"),      lastIsMine: c    }  }

Formatted prompt sender

content/contentScript.js
u = async function() {              const {                chatHistoryShort: t,                lastIsMine: a              } = await n(e);              N({                type: "info",                level: 135,                function_name: "parseHtmlunction",                messages: `last is mine :${a}`              });              let o = function(e, t) {                let n;                "default" == l ? n = e ? "As me, give a double texting utterance completing the following chat conversation flow. Use Emoji and my writing style and do not repeat the contents of the last utterance:\n\n" : "As me, give an utterance completing the following chat conversation flow. Use Emoji and my writing style:\n\n" : "custom" == l && (n = r);                let a = n + t + "\n\nMe: ";                return N({                  type: "info",                  level: 63,                  function_name: "createPrompt",                  messages: `${a}`                }), a              }(a, t);              A.setBusy(!0), await chrome.runtime.sendMessage({                message: "sendChatToGpt",                prompt: o              })            }

Formatted OpenAI forwarding handler

background/background.js
chrome.runtime.onMessage.addListener((function(s, r) {    "sendChatToGpt" === s.message && chrome.storage.local.get({      apiChoice: "webapp"    }, (n => {      "webapp" === n.apiChoice ? async function(s, r) {        try {          await (e?.cancel()), e?.releaseLock(), e = null;          const n = await async function() {            const e = await fetch("https://chat.openai.com/api/auth/session");            if (200 === e.status) {              const {                accessToken: t              } = await e.json();              return t            }            return null          }();          n ? await async function(s, r, n) {            const c = {                action: "next",                messages: [{                  id: t(),                  role: "user",                  content: {                    content_type: "text",                    parts: [r]                  }                }],                model: "text-davinci-002-render-sha",                parent_message_id: t(),                is_visible: !1              },              l = await fetch("https://chat.openai.com/backend-api/conversation", {                method: "POST",                headers: a(s),                body: JSON.stringify(c)              });            if (l.ok) try {              for (e = l.body.getReader();;) {                const {                  done: t,                  value: a                } = await e.read();                if (t) break;                const s = o((new TextDecoder).decode(a));                s && n({                  text: s                })              }            } catch (e) {              console.error(e), n({                error: {                  type: "unknown",                  message: e.message                }              })            } else 429 === l.status ? n({              error: {                type: "processingError",                message: "You have been rate-limited by ChatGPT. Please try again."              }            }) : n({              error: {                type: "processingError",                message: "Something went wrong fetching the answer from GPT; Statuscode: " + l.status + "," + l.statusText              }            })          }(n, s, r): r({            error: {              type: "noSessionToken",              message: '<a href="https://chat.openai.com/" target="_blank">Click here to log in to ChatGPT</a>!'            }          })        } catch (e) {          console.error(e), r({            error: {              type: "unknown",              message: e.message            }          })        }      }(s.prompt, (async function(e) {        try {          "popup" == s.from ? chrome.runtime.sendMessage({            message: "gptResponse",            response: e          }) : await chrome.tabs.sendMessage(r.tab.id, {            message: "gptResponse",            response: e          })        } catch (e) {          console.error("error sending message back to whatsapp", e)        }      })): async function(t, o) {        try {          await (e?.cancel()), e?.releaseLock(), e = null;          const {            apiKey: s,            modelSelect: r          } = await async function(e) {            const t = await new Promise((t => {                chrome.storage.local.get(e, (e => {                  t(e)                }))              })),              a = {};            return e.forEach((e => {              a[e] = t[e]            })), a          }(["apiKey", "modelSelect"]);          s && s.length > 0 ? await async function(t, o, s, r) {            const n = {                model: o,                messages: [{                  role: "user",                  content: s                }],                temperature: .5              },              c = await fetch("https://api.openai.com/v1/chat/completions", {                method: "POST",                headers: a(t),                body: JSON.stringify(n)              });            if (c.ok) try {              for (e = c.body.getReader();;) {                const {                  done: t,                  value: a                } = await e.read();                if (t) break;                const o = (new TextDecoder).decode(a),                  s = JSON.parse(o);                let n = null;                if (void 0 !== s.error) {                  let e = s.error.message;                  console.log("chatGetAnswerError:", e), n = e                } else try {                  n = s.choices[0].message.content.trim()                } catch (e) {                  n = s                }                n && r({                  text: n                })              }            } catch (e) {              console.error(e), r({                error: {                  type: "unknown",                  message: e.message                }              })            } else 429 === c.status ? r({              error: {                type: "processingError",                message: "You have been rate-limited by ChatGPT. Please try again."              }            }) : r({              error: {                type: "processingError",                message: "Something went wrong fetching the answer from GPT; Statuscode: " + c.status + "," + c.statusText              }            })          }(s, r, t, o): o({            error: {              type: "noApiKey",              message: "No api key found"            }          })        } catch (e) {          console.error(e), o({            error: {              type: "unknown",              message: e.message            }          })        }      }(s.prompt, (async function(e) {        try {          "popup" == s.from ? chrome.runtime.sendMessage({            message: "gptResponse",            response: e          }) : await chrome.tabs.sendMessage(r.tab.id, {            message: "gptResponse",            response: e          })        } catch (e) {          console.error("error sending message back to whatsapp", e)        }      }))    }))  }));  let e = null;  function t() {    return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (e => (e ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> e / 4).toString(16)))  }  function a(e) {    return new Headers({      "Content-Type": "application/json",      Authorization: "Bearer " + e    })  }  function o(e) {    const t = /"role":\s*"assistant".+?"parts":\s*\[(.*?)\]/g;    let a, o = "";    if (a = t.exec(e), !a) return null;    for (; null !== a;) o = a[1], a = t.exec(e);    return JSON.parse(o)  }
Remote services that can receive the generated prompt
    • chat.openai.com

    ChatGPT webapp endpoint receives the prompt when the extension is configured for webapp mode.

    • api.openai.com

    OpenAI chat-completions API receives the prompt when the extension is configured for API-key mode.

License activation sends WhatsApp phone number to Keygen license API.

When you activate a license in WaSendPlus, the extension sends your WhatsApp phone number to Keygen's license validation API as a device fingerprint.

The confirmed request goes to api.keygen.sh using the form-entered phone number.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

The user activates a license in the extension.

The extension did this

The extension sends the user's WhatsApp phone number as the license device fingerprint.

Captured request
POSTapi.keygen.sh/v1/accounts/telegramrocket-80ee1bb5/licenses/actions/validate-key
Fields in the request
  • Your WhatsApp phone number
    +1 415 555 0100 (illustrative)

    This value identifies the WhatsApp account or phone number and is used as the license device fingerprint.

What it can do

Permissions this extension asks for, as declared in version 0.3.14. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.

  • Read and change your data on openai.com

    https://*.openai.com/

  • Store data in your browser

    storage

  • Act on the current tab, but only after you click the extension

    activeTab

Updated 30 September 2026hegkopidlpiobmghdbcaflkdbbiegojg