Is FunPay Lite Bot safe?

Medium risk

FunPay Lite Bot is medium risk. The extension collects your FunPay account ID, username, and every feature toggle, encrypts it, and sends it to hbapi.funpaylitebot.com every 3 hours while a funpay.com tab is open. The AES key is hardcoded, blocking only casual inspection.

Lenvio Softwarev2.3.1Chrome Web Store
45Risk
Who publishes it

Lenvio Software - no other listings under this identity, 1 shared hostname

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
Lenvio Software
Registered address
Av. de Jaume III, 3, Centre, Palma, Islas Baleares 07012, ES
Registered contact
Lenvio Software

Shared hosts - 1 hostname

Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.

support.funpay.com
Also called by 2 other listings: Funpay extension, FunPay Tools

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

Publishers can request a review.

Findings

Extension encrypts your FunPay identity and settings, sends it every 3 hours

The extension collects your FunPay account ID, username, and every feature toggle, encrypts it, and sends it to hbapi.funpaylitebot.com every 3 hours while a funpay.com tab is open.

The AES key is hardcoded, blocking only casual inspection.

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

You keep a funpay.com tab open, or revisit it, while signed in.

No button click or setting change is needed to start the cycle.

The extension did this

The extension builds and sends an encrypted snapshot of your FunPay identity and settings.

It POSTs the encrypted payload to hbapi.funpaylitebot.com/api/v1/settings on a schedule the server can adjust.

When this fires

Every 3 hours

Repeats automatically about every 3 hours while a funpay.com tab is open, independent of anything you click.

Fields included in each settings snapshot
  • FunPay account ID
    7418032 (illustrative)

    Ties the snapshot to your specific FunPay account.

  • FunPay username
    seller_example (illustrative)

    Names the account the snapshot belongs to.

  • Extension version
    2.0.7

    Records which build of the extension sent the snapshot.

  • Feature toggles
    autoUp: true, customPrices: true, theme: dark

    Every on/off preference: auto-updates, custom prices, notification sound, stats panels, lot translation, theme, and more.

  • Plan features
    features: ["pro"]

    Which paid-plan features are active on your account.

  • Browser locale
    ru

    Records the language your browser is set to.

  • Custom background/notification flags
    customBackgroundSet: false

    Whether you've set a custom background image, dim level, or notification sound.

The code that does this

The content script assembles the snapshot on a timer; the background process encrypts and posts it

Readable version

Readable content script

content/c.js
  var di = "settingsSnapshotLastSuccessAt",    pi = "settingsSnapshotIntervalMs",    cd = 10800 * 1e3,    md = 1e3;  async function Ys() {    let e = await v(pi);    return typeof e == "number" && Number.isFinite(e) && e >= md ? e : cd  }  var ud = ["autoUp", "viewSellersPromo", "customPrices", "customNotify", "orderSound", "fastMessages", "showPurchasesStats", "exportPurchases", "showSalesStats", "exportSales", "showUnconfirmedSales", "showSectionCommission", "enableLotTranslate", "showRealLotPrices", "hideBalance", "theme"],    dd = "LBCustomBackground",    gd = "LBCustomBackgroundDim",    pd = "LBCustomNotifySound";  async function it(e = !1) {    try {      if (!e) {        let l = await v(di),          m = await Ys(),          p = Date.now();        if (typeof l == "number" && p - l < m) return      }      let t = await v("appData");      if (!t?.userId || !t?.userName) return;      let n = {};      for (let l of ud) {        let m = await v(l);        typeof m == "boolean" || typeof m == "string" || typeof m == "number" ? n[l] = m : n[l] = null      }      try {        let m = (await chrome.storage.local.get("authUser")).authUser;        n.features = m?.features ?? []      } catch {        n.features = []      }      let s = !1,        o = null;      try {        s = localStorage.getItem(dd) !== null;        let l = localStorage.getItem(gd);        if (l !== null) {          let m = Number(l);          Number.isFinite(m) && (o = m)        }      } catch {}      n.customBackgroundSet = s, n.customBackgroundDimPercent = o;      let a = !1;      try {        a = localStorage.getItem(pd) !== null      } catch {}      n.customNotifySoundSet = a;      let r = !1;      try {        let l = await chrome.storage.local.get("customOrderSoundDataUrl");        r = typeof l.customOrderSoundDataUrl == "string" && l.customOrderSoundDataUrl.length > 0      } catch {}      n.customOrderSoundSet = r, n.locale = chrome.i18n.getUILanguage();      let i = chrome.runtime.getManifest().version,        c = await chrome.runtime.sendMessage({          target: "background",          method: "sendSettingsSnapshot",          payload: {            funpayUserId: t.userId,            funpayUsername: t.userName,            extensionVersion: i,            settings: n          }        });      c?.ok && (await D(di, Date.now()), typeof c.nextIntervalMs == "number" && await D(pi, c.nextIntervalMs))    } catch (t) {      d(`Settings snapshot failed: ${t.message}`)    }  }  var gi = !1;  function fi() {    if (gi) return;    gi = !0;    let e = async () => {      try {        await it()      } catch {}      let t = await Ys();      setTimeout(e, t)    };    Ys().then(t => setTimeout(e, t))  }

Readable background script

background/b.js
  var vn = "e2a51f7f6ac34757847c8ab33bc7e885cf3957042ef4ff8bdcee2be087d75881",    Vt = Mt.debug && null ? null : vn;  var Ie = null;  async function An() {    if (Ie) return Ie;    let e = Dn(Vt);    if (e.length !== 32) throw new Error(`REPORT_AES_KEY_HEX must decode to 32 bytes (got ${e.length})`);    let r = e.buffer.slice(e.byteOffset, e.byteOffset + e.byteLength);    return Ie = await crypto.subtle.importKey("raw", r, {      name: "AES-GCM"    }, !1, ["encrypt"]), Ie  }  async function pe(e) {    let r = await An(),      t = crypto.getRandomValues(new Uint8Array(12)),      n = new TextEncoder().encode(e),      o = t.buffer.slice(t.byteOffset, t.byteOffset + t.byteLength),      s = n.buffer.slice(n.byteOffset, n.byteOffset + n.byteLength),      a = await crypto.subtle.encrypt({        name: "AES-GCM",        iv: o      }, r, s);    return `2:${Gt(t)}.${Gt(new Uint8Array(a))}`  }  function Gt(e) {    let r = "";    for (let t = 0; t < e.length; t++) r += String.fromCharCode(e[t]);    return btoa(r)  }  function Dn(e) {    if (e.length % 2 !== 0) throw new Error("REPORT_AES_KEY_HEX must have an even number of hex chars");    let r = new Uint8Array(e.length / 2);    for (let t = 0; t < r.length; t++) {      let n = parseInt(e.slice(t * 2, t * 2 + 2), 16);      if (Number.isNaN(n)) throw new Error(`REPORT_AES_KEY_HEX has a non-hex char near index ${t*2}`);      r[t] = n    }    return r  }  async function ta(e) {    try {      let r = {          v: 1,          u: e.funpayUserId,          n: e.funpayUsername,          ev: e.extensionVersion,          s: e.settings        },        t = await pe(JSON.stringify(r)),        n = `${E("hbapi",3001)}/api/v1/settings`,        o = await ee(B).catch(() => null),        s = u => P(n, {          method: "POST",          headers: {            "Content-Type": "text/plain",            ...u ? {              Authorization: `Bearer ${u}`            } : {}          },          body: t        }),        a = await s(o);      if (a.status === 401 && o) {        let f = (await chrome.storage.local.get("refreshToken")).refreshToken;        if (f) try {          let p = await B(f);          await z(p), a = await s(p.accessToken)        } catch {          a = await s(null)        } else a = await s(null)      }      let i = a.headers.get("X-S"),        c = i ? Number(i) : null;      return {        ok: a.ok,        nextIntervalMs: Number.isFinite(c) ? c : null,        status: a.status      }    } catch (r) {      return y(r), {        ok: !1,        nextIntervalMs: null,        status: null      }    }  }  var an = ta;
Why you can't catch this in DevTools

The extension encrypts the settings snapshot with AES-256-GCM before sending it, using a key that is hardcoded in the extension's own bundled code rather than issued per install or per user. The reproduction below is built with that same key and illustrative settings values (not the literal bytes of a captured production request), decrypting it yields the exact JSON structure the extension sends on the wire.

Decoded value
{  "v": 1,  "u": 7418032,  "n": "seller_example",  "ev": "2.0.7",  "s": {    "autoUp": true,    "viewSellersPromo": false,    "customPrices": true,    "customNotify": "vk",    "orderSound": true,    "fastMessages": false,    "showPurchasesStats": true,    "exportPurchases": false,    "showSalesStats": true,    "exportSales": true,    "showUnconfirmedSales": false,    "showSectionCommission": false,    "enableLotTranslate": false,    "showRealLotPrices": true,    "hideBalance": false,    "theme": "dark",    "features": [      "pro"    ],    "customBackgroundSet": false,    "customBackgroundDimPercent": null,    "customNotifySoundSet": false,    "customOrderSoundSet": false,    "locale": "ru"  }}
Reproduce it yourself

Decrypts a captured hbapi.funpaylitebot.com/api/v1/settings request body using the extension's own hardcoded AES-256-GCM key, or runs against a built-in illustrative example if no body is supplied.

Requires
  • Node.js 18+ (built-in crypto module only)
decrypt-settings-snapshot.js · js
// Decrypts a FunPay Lite Bot "settings snapshot" request body captured on the// wire to hbapi.funpaylitebot.com/api/v1/settings. The extension encrypts// this body with an AES-256-GCM key that ships hardcoded inside its own// background script, so no secret material needs to be supplied here.//// Usage://   1. Capture the request body posted to hbapi.funpaylitebot.com/api/v1/settings//      (format "2:<base64 iv>.<base64 ciphertext+tag>").//   2. node decrypt-settings-snapshot.js '<captured body>'//   3. The script prints the decrypted JSON: FunPay account ID, username,//      extension version, and the full settings map.//// Requires: Node.js 18+ (built-in `crypto` module only, no dependencies).const crypto = require("crypto");// Hardcoded in the extension's background script (background/b.js, var vn).const HARDCODED_KEY_HEX =  "e2a51f7f6ac34757847c8ab33bc7e885cf3957042ef4ff8bdcee2be087d75881";function decryptSnapshot(wireBody) {  if (!wireBody.startsWith("2:")) {    throw new Error('Unexpected format: expected a "2:<iv>.<ciphertext>" body');  }  const [ivB64, ctB64] = wireBody.slice(2).split(".");  if (!ivB64 || !ctB64) {    throw new Error('Malformed body: expected "<iv>.<ciphertext>" after the "2:" prefix');  }  const key = Buffer.from(HARDCODED_KEY_HEX, "hex");  const iv = Buffer.from(ivB64, "base64");  const combined = Buffer.from(ctB64, "base64");  // The extension's crypto.subtle.encrypt() call appends the 16-byte GCM  // auth tag to the end of the ciphertext, per the WebCrypto spec.  const authTag = combined.subarray(combined.length - 16);  const ciphertext = combined.subarray(0, combined.length - 16);  const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);  decipher.setAuthTag(authTag);  const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);  return JSON.parse(plaintext.toString("utf8"));}function main() {  const body = process.argv[2];  if (!body) {    console.error("Usage: node decrypt-settings-snapshot.js '<captured request body>'");    console.error("\nRunning with a built-in illustrative example instead:\n");    // Illustrative example, not a real captured request — built with the    // same hardcoded key and wire format the extension uses.    const exampleBody =      "2:+9CPCtWRJdu4vjUT.y+fSY71XXVmb0dA7wp6w9yD3KjlEGffC2wPd0vpTAqcpOFY6dsOgF4bJ1Unc4MNLt0IanA5gqtoGUuczHbvIlB1m1cGMuMYd9R9iOfTnE/XlMApd8i0AQl58C/1EHpWRQ2v3PHXlGI1eA3Puq5/a7SbojOyENoCZDe8aTJAGnZlh9Gl7RJUIGtc6HRl0ZhvS6MFWVmROzk9PrBx18lKXgFSlvhms6tUaCZVOH7oR6dAJcjnfBKTTk9zibWM/S7fDloquyS5NgOggHqoMPhd9f8M7+ycCfZmCxfRXw8prOjJ3lfCr1JkRTRgDrB0r76nvAyLpdndTYH/nRVoepgARi7XZDXkUBbg5593/i5d0qUDcJzZ4SX8gspp6GZdWHJc/3XpjAC6fEQv+k3v5g19Vckrd1wZn2XIr55zqFLnFKHJ8nKt0U/uEn5c+3t6pDa787EIx0Iqt8vamycZdaJaJt69OzQWFzoh/GCAgID0jd5GRTh/F5ul3Ixwj6po2QF4OXJJqdIIIZT1vnvyqHMnN3RiAm6V59jHOfzJNa+kYM3wP9r2i3L4aG81hhbfsJK2RI7xIQCHP6c2Y0tKhs75e3bEJJC4kE8VGRCgGuFEDO6Aeht4mjvrn+kBbqmoeOyHKZxNsaH2f/bzXXGRxNn1vSCnw7hGtgaoD/W5y+RpYhCM8IFqPc2B78PICC8+SUxjAJTsqCMQyHKs/eKAHqvJ3nsw4jKiEZxN0prPzIvPRnSbJCbPCYdwlO3pUgHhHbeb35Z48Rf4t";    console.log(JSON.stringify(decryptSnapshot(exampleBody), null, 2));    return;  }  console.log(JSON.stringify(decryptSnapshot(body), null, 2));}main();
How to run it
  1. 1Capture a body posted to hbapi.funpaylitebot.com/api/v1/settings.
  2. 2Run `node decrypt-settings-snapshot.js '<body>'`.
  3. 3Read the JSON: account ID, username, version, settings. Run with no argument for an example.
Remote destination used by the settings-snapshot path
    • hbapi.funpaylitebot.com

    Receives the encrypted settings-snapshot POST containing your FunPay account identity and feature-toggle settings.

Updated 30 September 2026amicfiagmpbgfiiopieeemlkblfeeeip