Is WAScheduler - WA Message Scheduler safe?

Medium risk

WADeck reads your WhatsApp phone number and sends URLs from private conversations to third-party link-preview servers.

The extension reads the signed-in user's phone number from WhatsApp Web's localStorage and transmits it, along with a persistent install ID and browser fingerprint, to an Alibaba Cloud logging endpoint. When a message containing a URL is sent, the extension forwards that URL to one of three external link-preview proxy servers — unaffiliated with WhatsApp — to fetch page metadata.

wadeck-publisherv2.0.6Chrome Web Store
45Risk

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

Publishers can request a review.

Findings

WhatsApp message URLs sent to preview proxy servers

MediumunwantedCWE-359Dynamic sandbox

When a WhatsApp Web message contains a URL, WAScheduler sends it to an external proxy: a GET to wajsapi.titanchat.com.br/v1/link-preview/fetch-data.png, with the outgoing message URL in the url query parameter.

What actually happens

You did this

You send a WhatsApp Web message that contains a URL.

The extension did this

The extension requests link-preview metadata from an external proxy endpoint.

The URL from the message is placed in the request's query string.

Captured request

GEThttps://wajsapi.titanchat.com.br/v1/link-preview/fetch-data.png?url=https://example.com/article

JSON preview metadata containing title, description, image, and URL fields.

Field sent to the link-preview proxy

  • Message URL
    https://example.com/article (illustrative)

    This is the web address typed into your WhatsApp message. It can reveal the site or content being discussed in that conversation.

Configured link-preview proxy hosts

    • wajsapi.titanchat.com.br

    Receives the message URL in a link-preview fetch request; this was the host named in the confirmed verification.

    • cobrancas.uppermesh.com.br
    • wppc-linkpreview.cloudtrix.com.br

    Configured as an alternate link-preview proxy host in the shipped extension code.

The code that does this

The injected WhatsApp helper sends the message URL to a preview proxy

Readable version

The content script injects the page helper

content.bundle.js
case 16:  i["default"].prototype.$injectEl = this.appLoc,  this.appendScriptTag(e.runtime.getURL("qpqp.js")),  this.appendScriptTag(e.runtime.getURL("inject.bundle.js")),  this.appendTagTo(this.appLoc, O.toobarId),  new i["default"]({    i18n: b["a"],    store: this.store,    render: function(e) {      return e(s["a"])    }  }).$mount("#" + O.toobarId)

Outgoing message text is scanned for the first web link

inject.bundle.js
t.prepareLinkPreview = function() {  var e = _asyncToGenerator(_regeneratorRuntime.mark((function e(t, r) {    var n, o, i, a;    return _regeneratorRuntime.wrap((function(e) {      while (1) switch (e.prev = e.next) {        case 0:          if (r.linkPreview) {            e.next = 2;            break          }          return e.abrupt("return", t);        case 2:          if (!r.linkPreview) {            e.next = 16;            break          }          if (n = "object" == _typeof(r.linkPreview) ? r.linkPreview : {}, o = "chat" === t.type ? t.body : "", !o) {            e.next = 16;            break          }          if (e.prev = 5, i = (0, f.findFirstWebLink)(o), !i) {            e.next = 12;            break          }          return e.next = 10, (0, f.fetchLinkPreview)(i);        case 10:          a = e.sent, (null == a ? void 0 : a.data) && (r.linkPreview = Object.assign(Object.assign({}, a.data), n));        case 12:          e.next = 16;          break;        case 14:          e.prev = 14, e.t0 = e["catch"](5);        case 16:          return "object" == _typeof(r.linkPreview) && (t.subtype = "url", t = Object.assign(Object.assign({}, t), r.linkPreview)), e.abrupt("return", t);        case 18:        case "end":          return e.stop()      }    }), e, null, [      [5, 14]    ])  })));  return function(t, r) {    return e.apply(this, arguments)  }}();

The proxy request is built with the message URL in the query string

inject.bundle.js
t.fetchRemoteLinkPreviewData = function() {  var e = _asyncToGenerator(_regeneratorRuntime.mark((function e(t) {    var r, n, o, i, a, u, s;    return _regeneratorRuntime.wrap((function(e) {      while (1) switch (e.prev = e.next) {        case 0:          if (!p[t]) {            e.next = 2;            break          }          return e.abrupt("return", (l("Link preview found in the cache", t), p[t]));        case 2:          r = new TextDecoder, n = f.length - 1;        case 4:          if (!(n >= 0)) {            e.next = 21;            break          }          return o = f[n], l("Fetching link preview using ".concat(o), t), i = "".concat(o, "/v1/link-preview/fetch-data.png?url=") + encodeURI(t), e.next = 10, (0, c.fetchDataFromPNG)(i).then((function(e) {            return r.decode(e)          })).then((function(e) {            return JSON.parse(e)          })).catch((function() {            return null          }));        case 10:          if (a = e.sent, null !== a && ("title" in a || "status" in a)) {            e.next = 14;            break          }          return l("The server ".concat(o, " is unavailable for link preview")), f.splice(n, 1), e.abrupt("continue", 18);        case 14:          if (a.title || 200 === a.status) {            e.next = 16;            break          }          return e.abrupt("continue", 18);        case 16:          return u = /^video/.test(a.mediaType), s = {            title: a.title,            description: a.description,            canonicalUrl: a.url,            matchedText: t,            richPreviewType: u ? 1 : 0,            doNotPlayInline: !u,            imageUrl: a.image          }, e.abrupt("return", (p[t] = s, setTimeout((function() {            delete p[t]          }), 3e5), s));        case 18:          n--, e.next = 4;          break;        case 21:          return e.abrupt("return", null);        case 22:        case "end":          return e.stop()      }    }), e)  })));  return function(t) {    return e.apply(this, arguments)  }}()

The configured server list is randomized before use

inject.bundle.js
var o = n(r(17833)),  i = r(91491),  a = r(14647),  u = r(52757),  s = r(97016),  c = r(24298),  l = (0, o.default)("WA-JS:link-preview"),  f = i.config.linkPreviewApiServers || ["https://cobrancas.uppermesh.com.br:8000", "https://wajsapi.titanchat.com.br", "https://wppc-linkpreview.cloudtrix.com.br"],  d = 140,  p = {},  v = {};! function(e) {  for (var t = e.length - 1; t > 0; t--) {    var r = Math.floor(Math.random() * (t + 1)),      n = [e[r], e[t]];    e[t] = n[0], e[r] = n[1]  }}(f)

Where it sends data

Destinations our analysis observed WAScheduler - WA Message Scheduler contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.

  • wajsapi.titanchat.com.brwidely used

    WAScheduler - WA Message Scheduler sends data to wajsapi.titanchat.com.br. A widely used service: 37 other extensions we have analyzed send data here.

  • wppc-linkpreview.cloudtrix.com.brwidely used

    WAScheduler - WA Message Scheduler sends data to wppc-linkpreview.cloudtrix.com.br. A widely used service: 35 other extensions we have analyzed send data here.

  • cobrancas.uppermesh.com.brwidely used

    WAScheduler - WA Message Scheduler sends data to cobrancas.uppermesh.com.br. A widely used service: 31 other extensions we have analyzed send data here.

  • extension-us.us-west-1.log.aliyuncs.comwidely used

    WAScheduler - WA Message Scheduler sends data to extension-us.us-west-1.log.aliyuncs.com. A widely used service: 28 other extensions we have analyzed send data here.

Updated 30 September 2026lbjgmhifiabkcifnmbakaejdcbikhiaj