Is Cursor Cat safe?
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.…
Who publishes itpeterp25926 - 1 other listing from the same operator, 1 of them carrying a finding
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.
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
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 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.
eventData.userId=fcff436a-0490-4684-8d65-aabf39050923- GiveFreely user IDfcff436a-0490-4684-8d65-aabf39050923
Lets GiveFreely connect events from different merchant pages back to the same browser profile.
- Anonymous user tokenStored as gf_anonymous_encrypted_token
Lets the extension keep using the same anonymous GiveFreely account after the first request.
- Selected charityselectedCharity=12-3456789; selectedCharityThirdPartyIdentifier=charity-24680 (illustrative)
Links the browser's shopping activity to the charity selected in the GiveFreely record.
- Merchant page URLhttps://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 identifiersmerchantId=120; domainId=456; provider=impact (illustrative)
Adds merchant, domain, provider, and partner context to the same user-linked request.
The shipped code creates a persistent user record and attaches it to events
Analytics event builder and POST sender
background.jsclass 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.jsclass 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.jsclass 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.jsasync 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") }}- 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
Cursor Cat starts its GiveFreely integration after the extension loads.
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.
Dynamic analysis observed partner behavioral JSON including `wfSecret`, `deviceUrl`, `dataUrl`, `vanityBaseUrl`, and `configRefreshInterval`.
Dynamic analysis observed global behavioral JSON including `eventsUrl`, `customStandownPolicy`, and `merchantExclusions` domain entries.
Dynamic analysis observed an event POST to the configured GiveFreely popup-events endpoint.
- Content-Type
- application/json
- Partner config filecursorcatprod.json
This file is specific to Cursor Cat's GiveFreely partner key and supplies settings for the extension's affiliate and popup behavior.
- Event destinationhttps://events.givefreely.com/popup
The config decides where later extension events are sent.
- Affiliate redirect basehttps://wild.link
The config supplies the base URL used when the extension generates affiliate links.
- Wildfire API endpointshttps://www.wildlink.me/webservice/v2/device
The config supplies API endpoints used for device and merchant data calls.
- Refresh interval3600 seconds
The server response can tell the extension when to refresh cached configuration.
- Merchant URL rulestrip.com/hotels/booknew
Domain and URL rules decide which shopping pages trigger popup, stand-down, or affiliate behavior.
- Wildfire app credentialswfAppId 572; wfSecret redacted
The config supplies app identifiers and a credential used by the extension's Wildfire integration.
The config service fetches, merges, and later uses remote settings
Readable config fetch and merge
deobfuscated/background.jsconst 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.jsasync 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.jsclass 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) }})();- 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.