Is Cursor Cat safe?

High risk

Cursor Cat is high risk. We observed a POST to events.givefreely.com/popup with eventData.userId=fcff436a-0490-4684-8d65-aabf39050923. Background code creates a GiveFreely user, stores the token, tags analytics with the ID; activation sends charity/merchant IDs.…

peterp25926v3.5.4Chrome Web Store
75Risk
Who publishes it

peterp25926 - 1 other listing from the same operator, 1 of them carrying a finding

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
peterp25926

Same store account

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

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

Publishers can request a review.

Findings

GiveFreely ID links merchant browsing across sites

We observed a POST to events.givefreely.com/popup with eventData.userId=fcff436a-0490-4684-8d65-aabf39050923.

Background code creates a GiveFreely user, stores the token, tags analytics with the ID; activation sends charity/merchant IDs.

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

You browse a merchant site or use the offer popup.

Supported merchant pages and offer activation paths provide the extension with the current domain, URL, merchant, and provider context.

The extension did this

The extension reuses a stable GiveFreely user ID in outgoing analytics and offer requests.

Dynamic analysis observed the same user ID in a POST to GiveFreely, and the code stores the anonymous token and user record locally.

Captured request
POSThttps://events.givefreely.com/popup
Body
eventData.userId=fcff436a-0490-4684-8d65-aabf39050923
Identity and browsing fields tied together by the code path
  • GiveFreely user ID
    fcff436a-0490-4684-8d65-aabf39050923

    Lets GiveFreely connect events from different merchant pages back to the same browser profile.

  • Anonymous user token
    Stored as gf_anonymous_encrypted_token

    Lets the extension keep using the same anonymous GiveFreely account after the first request.

  • Selected charity
    selectedCharity=12-3456789; selectedCharityThirdPartyIdentifier=charity-24680 (illustrative)

    Links the browser's shopping activity to the charity selected in the GiveFreely record.

  • Merchant page URL
    https://www.target.com/c/kitchen-dining/-/N-5xtsc (illustrative)

    Shows which shopping page or merchant site was active when the extension sent an event or generated an affiliate link.

  • Merchant identifiers
    merchantId=120; domainId=456; provider=impact (illustrative)

    Adds merchant, domain, provider, and partner context to the same user-linked request.

The code that does this

The shipped code creates a persistent user record and attaches it to events

Readable version

Analytics event builder and POST sender

background.js
class V {  constructor(e, t, r, i = 60) {    this.track = async (e, t, r = !1, i = !1) => {      const s = {        partner: `gfLib_${this._partnerApiKey}`,        eventType: e,        eventData: {          userId: r ? void 0 : this._userService?.user?.id,          libVersion: x(),          ...t        }      };      try {        const e = JSON.stringify(s, (e, t) => t instanceof Error ? {            name: t.name,            message: t.message,            stack: t.stack          } : t),          t = await this.getEventsUrl();        return t ? await this.BroadcastEvent(t, e, s, i) : (this._logger.info("Cant broadcast event. eventsUrl cant be retrieved from config", s, !0), !1)      } catch (e) {        return this._logger.error("Something unexpected happened when dispatching a gf event", {          error: e,          event: s        }, !0), !1      }    }, this.getEventsHistory = async () => {      const e = await k.get(S),        t = (e => {          const t = new Date;          return t.setTime(t.getTime() + 60 * e * 1e3), t.getTime()        })(-this._debounceWindowInMinutes),        r = (e || []).filter(e => e.createdAt > t);      return await k.set(S, r), r    }, this.shouldBroadcastEvent = async (e, t) => !!t || !(await this.getEventsHistory()).some(t => t.payload === e), this.pushEventToHistory = async e => {      const t = await this.getEventsHistory();      t.push({        createdAt: Date.now(),        payload: e      }), await k.set(S, t)    }, this._partnerApiKey = e, this._userService = t, this._debounceWindowInMinutes = i, this._logger = g.getInstance({      title: "Analytics"    }), this._configService = r  }  async BroadcastEvent(e, t, r, i) {    if (await this.shouldBroadcastEvent(t, i)) {      const i = await fetch(e, {        headers: {          "Content-Type": "application/json"        },        method: "POST",        body: t,        cache: "no-store"      });      return i.ok ? (await this.pushEventToHistory(t), !0) : (this._logger.error("Something unexpected happened when dispatching an event", {        status: i.status,        statusText: i.statusText,        event: r      }, !0), !1)    }    return this._logger.info("This event has already been broadcasted during the past hour", r, !0), !1  }  async getEventsUrl() {    const e = await this._configService.getCachedConfig();    return e?.eventsUrl  }}V.trackEvent = async (e, t, r = !1, i = !1) => {  await async function(e) {    return await u.runtime.sendMessage(e)  }({    type: p.TRACK_EVENT,    payload: {      eventType: e,      eventData: t,      anonymous: r,      skipThrottling: i    }  })};

Anonymous user API client

background.js
class Z {  constructor(e, t, r) {    this.libraryId = e, this.config = t, this.logger = r  }  async getAnonymousUserCommissions(e, t) {    try {      const r = new URLSearchParams(t).toString(),        i = await fetch(`${this.config.apiConfig.baseUri}/${this.config.apiConfig.getAnonymousUserComissionsPath}?${r}`, {          method: "GET",          headers: {            "Content-Type": "application/json",            "X-AnonymousUserToken": e          }        });      if (!i.ok) throw new Error(`Failed to fetch commissions. Status code: ${i.status} .${i.statusText}`);      return await i.json()    } catch (e) {      throw this.logger.error("[GiveFreelyApiClient] Error fetching anonymous user's commissions:", {        error: e      }), e    }  }  async createOrUpdateAnonymousUser(e, t) {    try {      const r = await fetch(`${this.config.apiConfig.baseUri}/${this.config.apiConfig.createAnonymousUserPath}?gfLibId=${this.libraryId}`, {        method: "PUT",        headers: {          "Content-Type": "application/json",          "X-GF-Platform": "GFLibrary",          "X-AnonymousUserToken": t ?? ""        },        body: JSON.stringify(e)      });      if (!r.ok) throw this.logger.error(`Failed to create/update anonymous user. Status code: ${r.status}. ${r.statusText}`), new Error(`Failed to create/update anonymous user. Status code: ${r.status}. ${r.statusText}`);      this.logger.info("[GiveFreelyApiClient] anonymous user created/updated successfully.");      const i = await r.json(),        s = r.headers.get("X-AnonymousUserToken");      return {        resultUser: {          ...i,          charity: {            ein: i.selectedCharity,            thirdPartyId: i.selectedCharityThirdPartyIdentifier,            name: void 0          }        },        resultToken: s      }    } catch (e) {      throw this.logger.error("[GiveFreelyApiClient] Error creating anonymous user:", {        error: e      }, !0), e    }  }}

Local reuse of the anonymous token and user record

background.js
class Y {  constructor(e, t) {    this._giveFreelyApiClient = e, this._logger = t, this.user = null  }  async resetUser() {    await k.remove(E), await k.remove(_)  }  async upsertUser(e) {    let t = await this.fetchUser();    try {      const r = {        selectedCharity: e?.ein,        selectedCharityThirdPartyIdentifier: e?.thirdPartyId      };      if (!await this.shouldCreateOrUpdateUser(t, r)) return t;      const i = await k.get(E),        {          resultUser: s,          resultToken: a        } = await this._giveFreelyApiClient.createOrUpdateAnonymousUser(r, i);      a && await k.set(E, a), s && (await k.set(_, s), t = s)    } catch (e) {      this._logger.error("Error creating/updating anonymous user:", e)    }    return this.user = t, t  }  async fetchUser() {    const e = await k.get(_);    return this.user = e, e  }  async shouldCreateOrUpdateUser(e, t) {    return !e || e.charity?.ein !== t.selectedCharity || e.charity?.thirdPartyId !== t.selectedCharityThirdPartyIdentifier  }}

Affiliate activation body includes user, charity, merchant, and URL fields

background.js
async generateAffiliateUrl(e, t, r, i, s, a) {  try {    const n = await fetch(`${this.activationUrl}?redirect=false`, {      method: "POST",      headers: {        "Content-Type": "application/json"      },      body: JSON.stringify({        provider: e.provider,        partnerId: i,        userId: r?.id || "",        ein: r?.charity?.ein || "",        tpi: r?.charity?.thirdPartyId || "",        url: t,        gfCommissionUniqueId: s,        merchantId: e.merchantId,        domainId: e.id,        providerCustom: a      })    });    if (!n.ok) throw new Error(`Failed to generate affiliate url. Status ${n.status}. ${n.statusText}`);    return (await n.json()).affiliateLink  } catch (e) {    throw this._logger.error("Failed to generate affiliate url", {      err: e    }), new Error("Failed to generate affiliate url")  }}
External services reached by this flow
    • events.givefreely.com

    Receives analytics events; dynamic analysis observed the popup endpoint with eventData.userId in the recorded body.

    • givefreely.com

    Provides the configured API and activation services used to create the anonymous user and generate affiliate links.

Remote GiveFreely config drives Cursor Cat behavior

Cursor Cat starts a GiveFreely integration (key `cursorcatprod`) and fetches config from cdn.givefreely.com: merchant data, analytics/affiliate endpoints, Wildfire settings, refresh timing, URL policy for popup/affiliate decisions.

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

Cursor Cat starts its GiveFreely integration after the extension loads.

The extension did this

The extension downloads behavioral JSON that controls merchant matching, event routing, affiliate URLs, and API settings.

The code merges the partner and global files before using the configuration.

Captured request
GEThttps://cdn.givefreely.com/data/behavioral/cursorcatprod.json

Dynamic analysis observed partner behavioral JSON including `wfSecret`, `deviceUrl`, `dataUrl`, `vanityBaseUrl`, and `configRefreshInterval`.

Captured request
GEThttps://cdn.givefreely.com/data/behavioral/global.json

Dynamic analysis observed global behavioral JSON including `eventsUrl`, `customStandownPolicy`, and `merchantExclusions` domain entries.

Captured request
POSThttps://events.givefreely.com/popup

Dynamic analysis observed an event POST to the configured GiveFreely popup-events endpoint.

Headers
Content-Type
application/json
Remote configuration fields observed or loaded by the extension
  • Partner config file
    cursorcatprod.json

    This file is specific to Cursor Cat's GiveFreely partner key and supplies settings for the extension's affiliate and popup behavior.

  • Event destination
    https://events.givefreely.com/popup

    The config decides where later extension events are sent.

  • Affiliate redirect base
    https://wild.link

    The config supplies the base URL used when the extension generates affiliate links.

  • Wildfire API endpoints
    https://www.wildlink.me/webservice/v2/device

    The config supplies API endpoints used for device and merchant data calls.

  • Refresh interval
    3600 seconds

    The server response can tell the extension when to refresh cached configuration.

  • Merchant URL rules
    trip.com/hotels/booknew

    Domain and URL rules decide which shopping pages trigger popup, stand-down, or affiliate behavior.

  • Wildfire app credentials
    wfAppId 572; wfSecret redacted

    The config supplies app identifiers and a credential used by the extension's Wildfire integration.

The code that does this

The config service fetches, merges, and later uses remote settings

Readable version

Readable config fetch and merge

deobfuscated/background.js
const K = "https://cdn.givefreely.com/data/behavioral/";async fetchPartnerConfig() {  const e = await d(async () => {    const e = await fetch(`${K}${this.partnerApiKey}.json`, {      cache: "no-store"    });    if (!e.ok) throw new Error(`Failed to fetch partner config: ${e.statusText}`);    return e.json()  }, this.getRetryOptions());  if (!e) throw new Error("Failed to fetch or get from cache partner config");  return this.partnerConfigRefreshInterval = e.configRefreshInterval || this.partnerConfigRefreshInterval, e}async fetchGlobalConfig() {  const e = await d(async () => {    const e = await fetch(`${K}global.json`, {      cache: "no-store"    });    if (!e.ok) throw new Error(`Failed to fetch global config => Status code: ${e.status} .${e.statusText}`);    return e.json()  }, this.getRetryOptions());  if (!e) throw new Error("Failed to fetch or get from cache global config", e);  return this.globalConfigRefreshInterval = e.configRefreshInterval || this.globalConfigRefreshInterval, e}async getConfig(e = !1) {  this.ensureInitialized();  let t = !1;  e && (t = await I());  const [r, i] = await Promise.all([N.get(w, this.fetchPartnerConfig.bind(this), this.partnerConfigRefreshInterval, t, e => e?.configRefreshInterval ?? null), N.get(v, this.fetchGlobalConfig.bind(this), this.globalConfigRefreshInterval, t, e => e?.configRefreshInterval ?? null)]);  if (!i.content) throw new Error("Global config is required");  if (!r.content) throw new Error("Partner config is required");  this.partnerConfig = r.content, this.globalConfig = i.content;  const s = this.mergeConfigs(JSON.parse(JSON.stringify(this.globalConfig)), JSON.parse(JSON.stringify(this.partnerConfig)));  if (!s) throw new Error("Was not able to merge configs");  return ((e, t, r) => {    const i = [...t.merchantExclusions ?? [], ...r.merchantExclusions ?? []];    e.merchantExclusions = i  })(s, this.globalConfig, this.partnerConfig), this.emitConfigChanges(s), s}

Readable event POST uses config-provided URL

deobfuscated/background.js
async BroadcastEvent(e, t, r, i) {  if (await this.shouldBroadcastEvent(t, i)) {    const i = await fetch(e, {      headers: {        "Content-Type": "application/json"      },      method: "POST",      body: t,      cache: "no-store"    });    return i.ok ? (await this.pushEventToHistory(t), !0) : (this._logger.error("Something unexpected happened when dispatching an event", {      status: i.status,      statusText: i.statusText,      event: r    }, !0), !1)  }  return this._logger.info("This event has already been broadcasted during the past hour", r, !0), !1}async getEventsUrl() {  const e = await this._configService.getCachedConfig();  return e?.eventsUrl}

Readable startup uses Cursor Cat partner key

deobfuscated/background.js
class Ze {  constructor(e) {    this.partnerApiKey = e, this.initialize = e => this._coreService.initialize(e), this.destroy = () => this._coreService.destroy(), this._coreService = new qe(e)  }}let Ye = !1,  Je = !1;(async () => {  try {    const e = new Ze("cursorcatprod");    await e.initialize()  } catch (e) {    console.error("[Cursor Cat] Give Freely initialization failed:", e)  }})();
External services controlled or referenced by the remote config
    • cdn.givefreely.com

    Serves the partner and global behavioral configuration JSON.

    • events.givefreely.com

    Receives popup and extension events at the config-provided `/popup` endpoint.

    • www.wildlink.me

    Wildfire device and data API endpoints referenced by the config.

    • wild.link

    Affiliate redirect base URL referenced by the config.

    • prod.cdnwgf.com

    Merchant feed host referenced by the partner configuration.

Updated 30 September 2026aeehekhncjhhmchjolinnihgdpapmljk