Is FunPay Lite Bot safe?
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.
Who publishes itLenvio Software - no other listings under this identity, 1 shared hostname
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.
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.
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
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 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.
Every 3 hours
Repeats automatically about every 3 hours while a funpay.com tab is open, independent of anything you click.
- FunPay account ID7418032 (illustrative)
Ties the snapshot to your specific FunPay account.
- FunPay usernameseller_example (illustrative)
Names the account the snapshot belongs to.
- Extension version2.0.7
Records which build of the extension sent the snapshot.
- Feature togglesautoUp: true, customPrices: true, theme: dark
Every on/off preference: auto-updates, custom prices, notification sound, stats panels, lot translation, theme, and more.
- Plan featuresfeatures: ["pro"]
Which paid-plan features are active on your account.
- Browser localeru
Records the language your browser is set to.
- Custom background/notification flagscustomBackgroundSet: false
Whether you've set a custom background image, dim level, or notification sound.
The content script assembles the snapshot on a timer; the background process encrypts and posts it
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;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.
{ "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" }}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.
- Node.js 18+ (built-in crypto module only)
// 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();- 1Capture a body posted to hbapi.funpaylitebot.com/api/v1/settings.
- 2Run `node decrypt-settings-snapshot.js '<body>'`.
- 3Read the JSON: account ID, username, version, settings. Run with no argument for an example.
- hbapi.funpaylitebot.com
Receives the encrypted settings-snapshot POST containing your FunPay account identity and feature-toggle settings.