Is Ultimate Web Scraper safe?
Ultimate Web Scraper is medium risk. Opening the side panel sends a PostHog event with your device ID, plan, account email, workspace ID, and view opened. DA captured 4 POSTs to us.i.posthog.com per open, bodies with the PostHog token, event type, device name, plan.…
Who publishes itHappy Panda Tech FZ-LLC - no other listings under this identity
Happy Panda Tech FZ-LLC - no other listings under this identity
What this publisher told the store about itself, and the other listings that told it the same thing.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Extension sends usage analytics to PostHog on every view
Opening the side panel sends a PostHog event with your device ID, plan, account email, workspace ID, and view opened.
DA captured 4 POSTs to us.i.posthog.com per open, bodies with the PostHog token, event type, device name, plan.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You open the Ultimate Web Scraper side panel.
This fires automatically on every load of sidepanel.html, before any interaction.
The extension sends your device ID, subscription plan, and account email to PostHog's US analytics servers.
Four POST requests to us.i.posthog.com were captured in our test session: an identify event with device details, followed by $pageview events for each view navigated.
HTTP 200. Event accepted by PostHog ingestion pipeline.
- Content-Type
- application/json
{ "api_key": "phc_k9Cv9VGCQCvHLdffgQUQrljroAB4UCYunFgtO094nAI", "event": "$pageview", "properties": { "$current_url": "Sidepanel", "view_name": "home", "automation_type": null, "automation_name": null, "device_id": "dev_a8f2c3e1b4d7", "device_name": "Chrome 146 • Mac", "is_pro": false, "has_subscription": false, "subscription_plan_name": "free", "app_version": "3.0-6.1.8" }}- Device IDdev_a8f2c3e1b4d7
A persistent identifier assigned to your browser profile in this extension. Ties all your sessions together across time.
- Device nameChrome 146 • Mac
Your browser version and operating system as a display string.
- Account email (Pro users)user@example.com
If you have a paid subscription, your account email is included in the identify call.
- Subscription planfree
Your plan tier is included in every event, allowing PostHog to segment users by subscription status.
- View namehome
The name of the panel view you navigated to (e.g. home, automations, email-extractor).
- App version3.0-6.1.8
Extension version string included in every event.
PostHog initialization and identify call (sidepanel.bundle.js)
PostHog configuration object
sidepanel.bundle.js:49315const posthogOptions = { api_host: "https://us.i.posthog.com", capture_exceptions: false, capture_pageview: false, // manual $pageview capture used instead autocapture: false, disable_external_dependency_loading: true, disable_session_recording: true, persistence: "localStorage", debug: false};PostHog initialization with hardcoded project API key
sidepanel.bundle.js:49329ReactDOM.createRoot(document.getElementById('root')).render( <PostHogProvider apiKey="phc_k9Cv9VGCQCvHLdffgQUQrljroAB4UCYunFgtO094nAI" options={posthogOptions} > <App /> </PostHogProvider>);User identify call on side panel mount
sidepanel.bundle.js:49262useEffect(() => { if (isInitialized && posthog && deviceId && !identified.current) { const traits = { device_id: deviceId, device_name: deviceInfo?.name || "Unknown Device", is_pro: isPro || false, has_subscription: isAuthenticated, subscription_plan_name: planName, subscription_email: userEmail ?? null, // Pro users only workspace_id: currentWorkspace?.id ?? null, app_version: "3.0-6.1.8" }; if (isPro && userId) traits.user_id = userId; posthog.identify(deviceId, traits); identified.current = true; }}, [isInitialized, posthog, deviceId, ...]);Manual $pageview event on each view navigation
sidepanel.bundle.js:44071posthog.capture("$pageview", { $current_url: viewDisplayName, page_title: viewDisplayName, view_name: currentView, // e.g. 'home', 'email-extractor' automation_type: automation?.id || null, automation_name: automation?.name || null});- us.i.posthog.com
PostHog product analytics ingestion endpoint (US region). Receives user identity and usage events. PostHog Inc. is a US-based analytics company.
License check sends a browser device profile
Submitting a license key makes Ultimate Web Scraper create or reuse a stored device ID, derive a device name from Chrome and OS, and post these with the key to api.pandaextract.com.
DA did not capture the POST; no key was entered.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You submit a license key in the extension interface.
The extension prepares a stored device ID and a Chrome-and-OS device name, then sends them for license validation.
- License keyUWS-7D4K-92MP-Q6ZT (illustrative)
Links the validation request to the license value you typed into the extension.
- Stored device ID9c2a5b6e-7f41-4b42-8a9d-3f177d0a2c11 (illustrative)
Lets the service recognize the same browser profile across separate license checks.
- Generated device nameChrome 126 • Linux (illustrative)
Describes your browser version and operating system as part of the license record.
No runtime request was captured in the test session because no license key was available to submit.
- Content-Type
- application/json
The shipped code creates the device profile and posts it to Panda Extract
Readable license-validation methods
deobfuscated/sidepanel.bundle.js static licenseURL = "https://api.pandaextract.com"; static async getOrCreateDeviceId() { let e = await th.retrieve("deviceId"); if (!e) { try { e = ih() } catch (t) { e = `device-${Date.now()}-${Math.random().toString(36).substring(2,9)}` } await th.save("deviceId", e) } return e } static async getDeviceInfo() { let e = await th.retrieve("deviceInfo"); return e || (e = { name: this.generateDefaultDeviceName(), firstRegistered: (new Date).toISOString() }, await th.save("deviceInfo", e)), e } static generateDefaultDeviceName() { try { const e = navigator.userAgent || ""; let t = ["Chrome"]; try { const r = e.match(/Chrome\/([0-9.]+)/); if (r && r[1]) { const e = r[1].split(".")[0]; e && (t[0] = `Chrome ${e}`) } } catch (e) {} try { e.includes("Windows") ? t.push("Windows") : e.includes("Mac") ? t.push("Mac") : e.includes("Linux") && t.push("Linux") } catch (e) {} return t.length > 1 ? t.join(" • ") : "Chrome Device" } catch (e) { return "Chrome Device" } } static isValidUUIDv4(e) { if (!e || "string" != typeof e) return !1; return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(e) } static async requestCheckLicenseKey({ key: e }) { const t = await this.getOrCreateDeviceId(), r = await this.getDeviceInfo(), n = await fetch(`${this.licenseURL}/api/devices/validate-devices`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: e, deviceId: t, deviceName: r.name }) }), a = await n.json(); return a.valid && a.deviceInfo && await th.save("deviceInfo", a.deviceInfo), a }- api.pandaextract.com
Receives license-validation requests and also serves /api/config for extension configuration.
User-started automation reads page content
Starting a scraping automation runs extraction code in the tab via Chrome scripting APIs.
That code reads the URL, title, meta fields, visible text, and selected DOM content, then writes rows into the extension's local IndexedDB.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You start a configured scraping automation.
The service worker runs extraction code in the target tab and stores the resulting rows locally.
- Page URLhttps://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html (illustrative)
Identifies the exact page being processed by the scraping workflow.
- Page titleA Light in the Attic | Books to Scrape (illustrative)
Adds human-readable context about the page being processed.
- Page contentA Light in the Attic £51.77 In stock (illustrative)
Contains visible text from the page body or main article area selected by the workflow.
- Selected element fieldsprice: £51.77; availability: In stock (illustrative)
Includes text, links, image URLs, and attributes chosen in the scraper configuration.
The extraction path injects page-reading code into the tab
Readable DOM text extractor
deobfuscated/background.bundle.js async extractTextFromPage(t, e, a) { try { const e = await chrome.scripting.executeScript({ target: { tabId: t }, func: function(t, e) { const a = t => t ? t.replace(/\s+/g, " ").replace(/\n\s*\n/g, "\n").trim() : ""; let r = document.title || ""; const n = ['meta[property="og:title"]', 'meta[name="twitter:title"]', "h1"]; for (const t of n) { const e = document.querySelector(t); if (e) { const t = "META" === e.tagName ? e.getAttribute("content") : e.textContent?.trim(); if (t && t.length > r.length) { r = t; break } } } let s = ""; const o = ['meta[name="description"]', 'meta[property="og:description"]', 'meta[name="twitter:description"]']; for (const t of o) { const e = document.querySelector(t); if (e && (s = e.getAttribute("content") || "", s)) break } let i = ""; const c = ["main", "article", '[role="main"]', ".content", ".post-content", ".entry-content", ".article-content", "#content", ".main-content"]; let l = null; for (const t of c) if (l = document.querySelector(t), l) break; if (l || (l = document.body), l) { const t = l.cloneNode(!0), e = ["script", "style", "nav", "header", "footer", ".navigation", ".nav", ".menu", ".sidebar", ".ads", ".advertisement", ".social-share", ".comments", ".comment", '[class*="ad-"]', '[id*="ad-"]', '[class*="social"]', '[class*="share"]']; if (e.forEach((e => { t.querySelectorAll(e).forEach((t => t.remove())) })), i = t.textContent || t.innerText || "", i = a(i), i.length < 100 && l !== document.body) { const t = document.body.cloneNode(!0); e.forEach((e => { t.querySelectorAll(e).forEach((t => t.remove())) })), i = a(t.textContent || t.innerText || "") } } const u = i ? i.split(/\s+/).filter((t => t.length > 0)).length : 0, d = (() => { const t = {}, e = ['meta[name="author"]', 'meta[property="article:author"]', '[rel="author"]', ".author", ".byline", '[class*="author"]']; for (const a of e) { const e = document.querySelector(a); if (e && ("META" === e.tagName ? t.author = e.getAttribute("content") : t.author = e.textContent?.trim(), t.author)) break } const a = ['meta[property="article:published_time"]', 'meta[name="date"]', 'meta[name="publish_date"]', "time[datetime]", ".publish-date", ".date", '[class*="date"]']; for (const e of a) { const a = document.querySelector(e); if (a && ("META" === a.tagName ? t.publishDate = a.getAttribute("content") : "TIME" === a.tagName ? t.publishDate = a.getAttribute("datetime") || a.textContent?.trim() : t.publishDate = a.textContent?.trim(), t.publishDate)) break } return t })(); return { title: a(r), description: a(s), content: i, wordCount: u, metadata: d, sourceUrl: e, extractedAt: Date.now() } }, args: [this.currentAction.config, a] }), r = e[0]?.result || { title: "", description: "", content: "", wordCount: 0, metadata: {} }; return { url: a, title: r.title, description: r.description, content: r.content, wordCount: r.wordCount, metadata: r.metadata, success: !0, extractedAt: Date.now(), itemCount: 1 } } catch (t) { return { url: a, title: "", description: "", content: "", wordCount: 0, metadata: {}, success: !1, error: t.message, extractedAt: Date.now(), itemCount: 0 } } }Readable tab metadata reader
deobfuscated/background.bundle.js async getTabInfo(t) { try { const e = await chrome.tabs.get(t); return { url: e.url, title: e.title, favIconUrl: e.favIconUrl } } catch (t) { return { url: null, title: null, favIconUrl: null } } }The extracted page rows are stored in the extension profile, so the data remains available to the extension after the automation finishes.
- Location
- IndexedDB database ultimateWebScraperDB
{ "database": "ultimateWebScraperDB", "rowFields": [ "tableDataId", "createdAt", "metadata.extractionType", "dataHash" ], "objectStores": [ "automations", "tableData", "tableRows" ]}The service worker starts the automation and writes rows
Readable START_AUTOMATION handler
deobfuscated/background.bundle.js handleStartAutomation(t, e, a) { const r = t.tabId || (e.tab ? e.tab.id : null), s = t.tableDataId || null, o = t.runnerType || null; if (!o) return a({ success: !1, error: "No runner type provided" }), !0; if ("pageExtractor" !== o && !r) return a({ success: !1, error: "No tab ID provided" }), !0; const { automationService: i } = n(363); return i.startAutomation({ tabId: r || null, tableDataId: s, actions: t.actions, runnerType: t.runnerType, config: t.config, metadata: t.metadata }, ((t, e) => { a({ success: !0, status: "started", automationId: t, tableDataId: e }) })).then((t => {})).catch((t => { a({ success: !1, status: "error", error: t.message || "Unknown error" }) })), !0 }Readable IndexedDB row writer
deobfuscated/background.bundle.js async storeTableRow(t) { await this.ensureConnection(); const e = { ...t, createdAt: Date.now() }; return new Promise(((t, a) => { const n = this.db.transaction([r.TABLE_ROWS], "readwrite").objectStore(r.TABLE_ROWS).add(e); n.onsuccess = () => { t(n.result) }, n.onerror = e => { n.error && "ConstraintError" === n.error.name ? (e.preventDefault(), t(null)) : a(n.error) } })) }Readable row flush loop
deobfuscated/background.bundle.js async processPendingItems() { if (!this.processing && 0 !== this.pendingItems.length) try { this.processing = !0; const t = this.pendingItems.splice(0, this.batchSize); let e = 0, a = 0, n = 0; for (const s of t) try { null === await r.db.storeTableRow(s) ? a++ : e++ } catch (t) { n++ } } finally { this.processing = !1, this.pendingItems.length > 0 && await this.processPendingItems() } }Dynamic analysis opened the side panel but did not configure or start an automation, so no scripting or tab API hooks were recorded during that run. The source code paths above show the behavior that runs after the automation is started.
+1 more finding not shown