Is Writesonic - SEO Keywords & AI Search Insights safe?

Medium risk

Writesonic is medium risk. The extension sends writing-feature interaction events to Google Analytics and Mixpanel with persistent analytics IDs; sign-up/upgrade events include a current_url value. Not triggered in our unauthenticated session; seen in shipped code.…

Writesonicv1.0.0.92Chrome Web Store
49Risk
Who publishes it

Writesonic, Inc. - 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.

Publisher
Writesonic
Declared legal entity
Writesonic, Inc.
Registered address
1209 Orange Street, Wilmington, DE 19801, US
Registered contact
Samanyou Garg

Same store account

1 other listing published from this account, 1k+ users between them, none of them carrying a finding.

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.

secure.helpscout.net
Also called by 6 other listings, including TeleGo, Everhour

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

Writing actions send analytics identifiers

The extension sends writing-feature interaction events to Google Analytics and Mixpanel with persistent analytics IDs; sign-up/upgrade events include a current_url value.

Not triggered in our unauthenticated session; seen in shipped code.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You use a supported Writesonic writing feature or account-action control.

The code includes handlers for Gmail, Twitter, LinkedIn, sign-up, and upgrade interactions.

The extension did this

The extension turns the click into an analytics event and sends it to two analytics services.

Some account-action events include a current_url value, and Mixpanel events include a distinct identifier.

Fields assembled for analytics events
  • Interaction name
    CS_CE_Gmail_Write_Button_Click

    Shows which extension feature you used, such as a Gmail write click or LinkedIn message click.

  • Current URL
    https://app.writesonic.com/settings/billing?utm_source=chatsonic-chrome-extension

    For sign-up and upgrade events, this connects the analytics event to the page or destination URL involved in the action.

  • Google Analytics client ID
    8b0f7d14-35a6-44e0-9e47-d48a4b6fd7d2 (illustrative)

    Lets Google Analytics connect multiple extension events to the same browser profile.

  • Mixpanel distinct ID
    user_742918 (illustrative)

    Can identify a signed-in Writesonic account or an anonymous extension profile across events.

  • Tone setting
    Professional

    Adds feature-specific context about how you used the writing tool.

Captured request
POSThttps://www.google-analytics.com/mp/collect?measurement_id=G-W40B6C5P30&credential=<redacted>

The code posts a JSON body with client_id and events; no successful dynamic request body was recorded for this claim.

Captured request
POSThttps://api.mixpanel.com/track

The background script posts the Mixpanel event body; no successful extension-originated body was recorded during the unauthenticated dynamic session.

Headers
accept
text/plain
content-type
application/json
The code that does this

Feature clicks are mapped to analytics events and sent to two services

Readable version

Click handlers call trackAnalytic

js/content_main.js
async onClickWriteBtn({  text: t,  toneOfVoice: e}) {  this.parent.typeBtnClick = "write_msg", this.helper.getTextFormElements(), c.trackAnalytic("gmail_write_btn_click", {    toneOfVoice: e  }), this.parent.args.onClickWriteBtn(t, e)}async onClickEWidgetWriteBtn({  e: t,  text: e,  toneOfVoice: i}) {  const n = t.target.closest("form").querySelector(".msg-form__contenteditable");  if (!n) return void this.services.execEmailWSEndWriting();  this.parent.typeBtnClick = "mail", this.parent.activeTextBox = n, this.helpers.addLoaderMessageBox(this.parent.activeTextBox);  const s = {    token: c.token,    type: "linkedin_message",    data: {      toneOfVoice: i,      msg: e    }  };  c.trackAnalytic("linkedin_write_msg_btn_click", {    toneOfVoice: i  }), this.parent.socialIframe.sendMessageToIframeForce("integ_ws_msg", s)}

Analytics router and event dispatch

js/content_main.js
track(t, e = {}) {  switch (t) {    case "signup":      "native" === e.scope ? fc.preSignUpBtnClickSaveStorage(e?.href) : fc.preSignUpBtnClick(e?.href);      break;    case "upgrade":      "native" === e.scope ? fc.upgradeBtnClickSaveStorage(e.email, e.fullName, e.href) : fc.upgradeBtnClick(e.email, e.fullName, e.href);      break;    case "gmail_write_btn_click":      fc.gmailWriteBtnClick(e.toneOfVoice);      break;    case "gmail_stop_btn_click":      fc.gmailStopBtnClick(e.toneOfVoice);      break;    case "email_summary_genereted":      fc.gmailSummaryGenereted();      break;    case "email_reply_btn_click":      fc.gmailReplyToEmailBtnClick(e.toneOfVoice);      break;    case "twitter_write_btn_click":      fc.twitterWriteBtnClick(e.toneOfVoice);      break;    case "twitter_stop_btn_click":      fc.twitterStopBtnClick("");      break;    case "twitter_reload_btn_click":      fc.twitterReloadBtnClick("");      break;    case "twitter_thread_reply":      fc.twitterReplyToThread();      break;    case "twitter_thread_summary":      fc.twitterThreadSummary();      break;    case "linkedin_write_btn_click":      fc.linkedinWritePostBtnClick(e.toneOfVoice);      break;    case "linkedin_write_msg_btn_click":      fc.linkedinWriteMsgBtnClick(e.toneOfVoice)  }}async exec(t) {  await this.sendRequestGA4(t), this.sendRequestMixpanel(t)}

Google Analytics and Mixpanel payload construction

js/content_main.js
async sendRequestGA4(t) {  const e = {    name: t.name  };  if (t.params && (e.params = t.params), !this.gaClientId) {    const t = await chrome.storage.local.get(null);    if (t.ga_client_id) this.gaClientId = t.ga_client_id;    else {      const t = W();      this.gaClientId = t;      const e = {        ga_client_id: t      };      await chrome.storage.local.set(e)    }  }  fetch(`https://www.google-analytics.com/mp/collect?measurement_id=${this.measurementId}&credential=<redacted>`, {    method: "POST",    body: JSON.stringify({      client_id: this.gaClientId,      events: [e]    })  })}async sendRequestMixpanel(t) {  try {    let e = {};    const i = t.name;    t.params && (e = t.params || e);    const n = await this.getMixpanelDistinctID();    try {      const t = [{        event: i,        properties: {          token: mo.mixpanel_cred.project_token,          distinct_id: n,          $insert_id: W(),          time: Date.now(),          ...e        }      }];      xo({        reason: lt.EXEC_MP_TRACK,        data: {          body: t        }      })    } catch (t) {      console.log(t)    }  } catch (t) {    console.log(t)  }}

Background Mixpanel network send

js/background.js
async execRequestToMixpanel(e) {  try {    const t = JSON.stringify(e);    fetch(r.mixpanel_cred.base_url + "/track", {      method: "POST",      headers: {        accept: "text/plain",        "content-type": "application/json"      },      body: t    })  } catch (e) {    console.log(e)  }}
Analytics destinations
    • www.google-analytics.com

    Receives Measurement Protocol events with the extension's analytics client ID and event names.

    • api.mixpanel.com

    Receives Mixpanel track events with distinct_id, event name, insert ID, timestamp, and event-specific fields.

Sign-up analytics include IP address and page URL

Sign-up and upgrade actions can make the extension resolve your public IP via db-ip.com, then attach that IP and a page URL to events sent to Google Analytics and Mixpanel.

Not triggered in our unauthenticated session; seen in shipped code.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-359
Source
Dynamic sandbox
What actually happens
You did this

You click a sign-up or upgrade action in the extension flow.

The code routes those actions through named analytics events for sign-up and upgrade clicks.

The extension did this

The extension looks up your public IP address and attaches it to an analytics event with a page URL.

Upgrade events also include the email address and full name values passed into the upgrade handler.

Fields assembled for sign-up and upgrade analytics
  • Your public IP address
    198.51.100.23 (illustrative)

    Identifies the network address seen by websites and services when the event is created.

  • Page URL
    https://app.writesonic.com/settings/billing?utm_source=chatsonic-chrome-extension

    Shows which page or destination URL was connected to the sign-up or upgrade action.

  • Email address
    alex.chen@example.com (illustrative)

    Can identify the account connected to an upgrade action when that value is passed to the handler.

  • Full name
    Alex Chen (illustrative)

    Can identify the person connected to an upgrade action when that value is passed to the handler.

Captured request
GEThttps://api.db-ip.com/v2/free/self

The code reads the JSON ipAddress field from this response; the unauthenticated dynamic session did not record this request.

Captured request
POSThttps://www.google-analytics.com/mp/collect?measurement_id=G-W40B6C5P30&credential=<redacted>

The code posts a JSON body with client_id and events; no successful dynamic request body was recorded for this claim.

The code that does this

The shipped code builds sign-up and upgrade analytics with IP lookup

Readable version

Sign-up and upgrade event builders

js/content_main.js
async preSignUpBtnClickSaveStorage(t) {  const e = {    name: "CS_CE_Sign_Up_Button_Click",    params: {      current_url: t,      ipAddress: await this.#ce() || ""    }  };  await this.#le(e)}async preSignUpBtnClick(t) {  const e = {    name: "CS_CE_Sign_Up_Button_Click",    params: {      current_url: t,      ipAddress: await this.#ce() || ""    }  };  await gc.exec(e)}async upgradeBtnClick(t, e, i) {  const n = {    name: "CS_CE_Upgrade_Button_Click",    params: {      current_url: i,      ipAddress: await this.#ce() || "",      email: t,      fullName: e    }  };  gc.exec(n)}async upgradeBtnClickSaveStorage(t, e, i) {  const n = {    name: "CS_CE_Upgrade_Button_Click",    params: {      current_url: i,      ipAddress: await this.#ce() || "",      email: t,      fullName: e    }  };  await this.#le(n)}

Public-IP lookup helper

js/content_main.js
async #ce() {  try {    const t = await fetch("https://api.db-ip.com/v2/free/self");    return (await t.json()).ipAddress  } catch (t) {    return null  }}

Google Analytics and Mixpanel dispatch

js/content_main.js
async exec(t) {  await this.sendRequestGA4(t), this.sendRequestMixpanel(t)}async sendRequestGA4(t) {  const e = {    name: t.name  };  if (t.params && (e.params = t.params), !this.gaClientId) {    const t = await chrome.storage.local.get(null);    if (t.ga_client_id) this.gaClientId = t.ga_client_id;    else {      const t = W();      this.gaClientId = t;      const e = {        ga_client_id: t      };      await chrome.storage.local.set(e)    }  }  fetch(`https://www.google-analytics.com/mp/collect?measurement_id=${this.measurementId}&credential=<redacted>`, {    method: "POST",    body: JSON.stringify({      client_id: this.gaClientId,      events: [e]    })  })}async sendRequestMixpanel(t) {  try {    let e = {};    const i = t.name;    t.params && (e = t.params || e);    const n = await this.getMixpanelDistinctID();    try {      const t = [{        event: i,        properties: {          token: mo.mixpanel_cred.project_token,          distinct_id: n,          $insert_id: W(),          time: Date.now(),          ...e        }      }];      xo({        reason: lt.EXEC_MP_TRACK,        data: {          body: t        }      })    } catch (t) {      console.log(t)    }  } catch (t) {    console.log(t)  }}
External hosts named by the code paths
    • api.db-ip.com

    Receives the IP lookup request and returns the public IP address used in the analytics event.

    • www.google-analytics.com

    Receives the Measurement Protocol analytics event.

    • api.mixpanel.com

    Receives the Mixpanel tracking event relayed by the background script.

Updated 30 September 2026kchaponcodemjigejilffhfchecpgdpf