Is CaptchaSonic: Automatic Captcha Solver safe?

High risk

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.

soniccaptchav0.3.3Chrome Web Store
72Risk
Who publishes it

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.

Publisher
soniccaptcha

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.

projudi.tjba.jus.br
Also called by 3 other listings, including MinutaIA Conecta, Presto

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
What actually happens
You did this

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 did this

The extension builds a profile event and sends it to CaptchaSonic.

That event includes the API key, browser identifiers, username, and installed-extension inventory.

Captured request
POSThttps://api.captchasonic.com/ingest
Headers
apikey
<redacted>
X-Device-Id
8f3a2c24-7b1d-4f3f-a7c0-5d6e9f0a1b2c (illustrative)
Content-Type
application/x-protobuf
X-Extension-Version
0.3.2
Fields assembled into the protobuf ingest payload
  • Event name
    apikey_update

    Shows which action caused this upload.

  • CaptchaSonic API key
    redacted CaptchaSonic API key

    Ties the profile event to the service key you entered.

  • Device identifier
    8f3a2c24-7b1d-4f3f-a7c0-5d6e9f0a1b2c (illustrative)

    Lets the service recognize the same browser installation across later requests.

  • Browser fingerprint
    d4b1b6b0e3e4490aa8f2f44c7b2f8d31 (illustrative)

    Adds a second stable identifier derived from browser and device signals.

  • Account username
    captcha-user@example.com (illustrative)

    Links the profile event to the username returned by API-key validation.

  • Time fields
    timestamp=1720989600000, date=2024-07-14T18:00:00.000Z (illustrative)

    Records when the profile event was generated.

  • Installed items
    id=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 counts
    totalCount=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 code that does this

The background worker validates the key, builds the inventory, and posts the protobuf payload

Readable version

Settings change trigger

background.js
extensionApi.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.js
async 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.js
function 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;}
Remote service receiving the profile event
    • 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.

Updated 30 September 2026dkkdakdkffippajmebplgnpmijmnejlh