Is CaptchaSonic: Automatic Captcha Solver safe?
CaptchaSonic uploads your installed-extension inventory and stable browser fingerprint after API-key setup.
When a user adds or updates the API key, CaptchaSonic validates it and then sends an ingest event to its API. That event includes the extension's API key, device ID, stable browser fingerprint, username, timestamp, and a list of other installed extensions with names, IDs, versions, enabled status, install types, and counts.
Who publishes itsoniccaptcha - no other listings under this identity, 1 shared hostname
soniccaptcha - 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
API-key setup sends installed-extension inventory
Adding the CaptchaSonic API key makes it validate, then post an ingest event to api.captchasonic.com with the key, device ID, browser fingerprint, username, and installed extensions/themes/apps (IDs, names, versions).
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You save a CaptchaSonic API key in the extension settings.
The background worker detects that the stored settings changed and validates the new key.
The extension builds a profile event and sends it to CaptchaSonic.
That event includes the API key, browser identifiers, username, and installed-extension inventory.
- apikey
- <redacted>
- X-Device-Id
- 8f3a2c24-7b1d-4f3f-a7c0-5d6e9f0a1b2c (illustrative)
- Content-Type
- application/x-protobuf
- X-Extension-Version
- 0.3.2
- Event nameapikey_update
Shows which action caused this upload.
- CaptchaSonic API keyredacted CaptchaSonic API key
Ties the profile event to the service key you entered.
- Device identifier8f3a2c24-7b1d-4f3f-a7c0-5d6e9f0a1b2c (illustrative)
Lets the service recognize the same browser installation across later requests.
- Browser fingerprintd4b1b6b0e3e4490aa8f2f44c7b2f8d31 (illustrative)
Adds a second stable identifier derived from browser and device signals.
- Account usernamecaptcha-user@example.com (illustrative)
Links the profile event to the username returned by API-key validation.
- Time fieldstimestamp=1720989600000, date=2024-07-14T18:00:00.000Z (illustrative)
Records when the profile event was generated.
- Installed itemsid=aapbdbdomjkkjkaonfhkkikfgjllcleb, name=Google Docs Offline, type=extension, enabled=true, version=1.76.0, installType=normal (illustrative)
Lists other installed extensions, themes, and apps with IDs, names, enabled state, versions, type, and install type.
- Inventory countstotalCount=12, enabledCount=10, disabledCount=2, types.extensions=9, types.themes=1, types.apps=2 (illustrative)
Summarizes how many installed items are present and how many are enabled or disabled.
The background worker validates the key, builds the inventory, and posts the protobuf payload
Settings change trigger
background.jsextensionApi.storage.onChanged.addListener(async (changes, area) => { if (area !== "local" || !changes.settings) return; const oldApiKey = changes.settings.oldValue?.APIKEY; const newApiKey = changes.settings.newValue?.APIKEY; const oldLastIngest = changes.settings.oldValue?.lastProfileIngestSent; const newLastIngest = changes.settings.newValue?.lastProfileIngestSent; if (!(oldApiKey === newApiKey && oldLastIngest !== newLastIngest) && newApiKey && newApiKey !== oldApiKey) { try { const balance = await (await fetch(ENDPOINTS.balance(newApiKey), { method: "GET", headers: { "Content-Type": "application/json", APIKEY: newApiKey }, })).json(); if (balance?.code === 1) return; if (balance?.username) { const settingsWithUsername = { ...changes.settings.newValue, username: balance.username }; await extensionApi.storage.local.set({ settings: settingsWithUsername }); } const deviceId = await N(); if (deviceId) { await w("apikey_update", deviceId, { skipValidation: true, username: balance?.username }); } await openpanel.track("api_key_set", { timestamp: Date.now(), username: balance?.username }); await o({ event: "api_key_set", desc: "API Key updated successfully", time: Date.now(), type: "success" }); } catch (error) { console.error("Error validating API key or sending profile:", error); await openpanel.track("api_key_error", { error: error.message, timestamp: Date.now() }); } }});Ingest payload and extension inventory
background.jsasync function w(eventName, deviceId, options = {}) { try { const { settings } = await extensionApi.storage.local.get("settings"); const apiKey = settings?.APIKEY; let username = settings?.username || options.username; if (!apiKey) return; if (!options.skipValidation) { const balance = await (await fetch(ENDPOINTS.balance(apiKey), { method: "GET", headers: { "Content-Type": "application/json", APIKEY: apiKey }, })).json(); if (balance?.code === 1) return; if (balance?.username && (username = balance.username, username !== settings?.username)) { const updatedSettings = { ...settings, username }; await extensionApi.storage.local.set({ settings: updatedSettings }); } } const lastProfileIngestSent = settings?.lastProfileIngestSent; const now = Date.now(); const oneDayMs = 1440 * 60 * 1000; if (eventName !== "apikey_update" && lastProfileIngestSent && now - lastProfileIngestSent < oneDayMs) return; const extensionProfile = await getExtensionProfile(); const stableFingerprint = await S(); const ingestPayload = { event: eventName, apiKey, deviceId, stableFingerprint: stableFingerprint || "", timestamp: Date.now(), date: new Date().toISOString(), extensionProfile, username, }; const verificationError = ingest.IngestPayload.verify(ingestPayload); if (verificationError) { console.error("Ingest payload verification failed:", verificationError); return; } const protobufMessage = ingest.IngestPayload.create(ingestPayload); const protobufBody = ingest.IngestPayload.encode(protobufMessage).finish(); const response = await fetch(ENDPOINTS.ingest, { method: "POST", headers: { "Content-Type": "application/x-protobuf", apikey: apiKey, "X-Device-Id": deviceId, "X-Extension-Version": extensionApi.runtime.getManifest().version, }, body: protobufBody, }); if (response.ok) { const updatedSettings = { ...settings, lastProfileIngestSent: now, ...(username ? { username } : {}) }; await extensionApi.storage.local.set({ settings: updatedSettings }); await openpanel.track("profile_ingest_success", { method: "protobuf_ingest", username }); } } catch (error) { console.error("Error sending extension profile for ${eventName} event:", error); }}async function getExtensionProfile() { try { const allItems = await chrome.management.getAll(); const ownExtensionId = chrome.runtime.id; const otherItems = allItems.filter((item) => item.id !== ownExtensionId); const byType = { extension: [], theme: [], hosted_app: [], packaged_app: [], legacy_packaged_app: [] }; otherItems.forEach((item) => { if (byType[item.type]) { byType[item.type].push({ id: item.id, name: item.name, enabled: item.enabled, version: item.version }); } }); return { totalCount: otherItems.length, enabledCount: otherItems.filter((item) => item.enabled).length, disabledCount: otherItems.filter((item) => !item.enabled).length, types: { extensions: byType.extension.length, themes: byType.theme.length, apps: byType.hosted_app.length + byType.packaged_app.length + byType.legacy_packaged_app.length, }, extensions: otherItems.map((item) => ({ id: item.id, name: item.name, type: item.type, enabled: item.enabled, version: item.version, installType: item.installType, })), }; } catch (error) { console.error("Error getting extension profile:", error); return { totalCount: 0, enabledCount: 0, disabledCount: 0, types: { extensions: 0, themes: 0, apps: 0 }, extensions: [], error: error.message }; }}Device ID and browser fingerprint helpers
background.jsfunction makeUuid() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => { const value = Math.random() * 16 | 0; return (char === "x" ? value : value & 3 | 8).toString(16); });}async function N() { const stored = await extensionApi.storage.local.get(["settings", "metaid", "device_instance_id"]); const settings = stored.settings || {}; if (settings.metaid) { if (stored.metaid || stored.device_instance_id) { await extensionApi.storage.local.remove(["metaid", "device_instance_id"]); } return settings.metaid; } let deviceId = stored.metaid || stored.device_instance_id; if (deviceId) { const updatedSettings = { ...settings, metaid: deviceId }; await extensionApi.storage.local.set({ settings: updatedSettings }); await extensionApi.storage.local.remove(["metaid", "device_instance_id"]); return deviceId; } deviceId = makeUuid(); const updatedSettings = { ...settings, metaid: deviceId }; await extensionApi.storage.local.set({ settings: updatedSettings }); return deviceId;}async function S() { if (await k.init(), k.hasSessionFingerprint()) { const sessionFingerprint = k.getSessionFingerprint(); if (sessionFingerprint) return sessionFingerprint; } try { await T(); await new Promise((resolve) => setTimeout(resolve, 100)); const response = await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error("Timeout")), 5000); chrome.runtime.sendMessage({ type: "GET_FINGERPRINT" }, (message) => { clearTimeout(timeout); chrome.runtime.lastError ? reject(chrome.runtime.lastError) : resolve(message); }); }); if (response && response.visitorId) { k.setSessionFingerprint(response.visitorId); return response.visitorId; } console.warn("FingerprintJS response missing visitorId:", response); } catch (error) { console.error("Failed to get stable visitorId:", error); } return null;}- api.captchasonic.com
Receives the /balance API-key validation request and the /ingest protobuf upload containing the API key, device ID, fingerprint, username, and installed-extension inventory.
Where it sends data
Destinations our analysis observed CaptchaSonic: Automatic Captcha Solver 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.
- api.captchasonic.com
CaptchaSonic: Automatic Captcha Solver sends data to api.captchasonic.com. No other extension we have analysed sends data here.