Is CSS Peeper safe?
CSS Peeper is medium risk. CSS Peeper bundles GiveFreely's SDK, which runs at startup to create a permanent anonymous profile with a locally stored auth token, captured leaving in a GiveFreely request. Never rotated, it rides every later request as a persistent ID.…
Who publishes itSparkglare Sp. Z o.o. - no other listings under this identity
Sparkglare Sp. Z o.o. - no other listings under this identity
What this publisher told the store about itself, and the other listings that told it the same thing.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Bundled GiveFreely SDK creates a persistent ID on install, sent to its servers
CSS Peeper bundles GiveFreely's SDK, which runs at startup to create a permanent anonymous profile with a locally stored auth token, captured leaving in a GiveFreely request.
Never rotated, it rides every later request as a persistent ID.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You install or start CSS Peeper.
No click, sign-in, or setup screen is involved, the bundled GiveFreely SDK begins running the moment the background service worker loads.
The extension creates a permanent anonymous user profile and transmits its ID off the device.
A PUT request to GiveFreely's API creates the profile and returns an authentication token, which is stored locally and then resent to GiveFreely's servers on later requests.
GiveFreely init and anonymous-user creation at service-worker startup (static/background/index.js)
Deobfuscated — SDK instantiated and initialized at module load
let u = new i.GiveFreelyService("csspeeperprod");(async () => { try { await u.initialize() } catch (e) { console.error("Failed to initialize GiveFreely:", e) }})();Deobfuscated — anonymous-user creation call
async createOrUpdateAnonymousUser(e, t) { try { let 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 Error(`Failed to create/update anonymous user. Status code: ${r.status}. ${r.statusText}`); let a = await r.json(), i = r.headers.get("X-AnonymousUserToken"); return { resultUser: { ...a, charity: { ein: a.selectedCharity, thirdPartyId: a.selectedCharityThirdPartyIdentifier, name: void 0 } }, resultToken: i } } catch (e) { throw this._logger?.error("Error creating anonymous user:", { error: e }, true), e }}Holds the anonymous profile id (becomes userId in every GiveFreely event) and the bearer token that authenticates as that profile.
- Location
- chrome.storage.local, keys gf_anonymous_user_info and gf_anonymous_encrypted_token
{ "gf_anonymous_user_info": { "id": "b848c461-7053-4619-acf1-7e479320321d", "selectedCharity": null, "selectedCharityThirdPartyIdentifier": null }, "gf_anonymous_encrypted_token": "<X-AnonymousUserToken value returned by the create-user API; used as a bearer credential on later GiveFreely requests>"}Stored user ID auto-attached to every GiveFreely event report (index.js:2259-2320)
class Analytics { track = async (eventType, extraData, anonymous = false, skipThrottling = false) => { let envelope = { partner: `gfLib_${this._partnerApiKey}`, eventType, eventData: { userId: anonymous ? undefined : this._userService?.user?.id, libVersion: getLibVersion(), ...extraData } }; const body = JSON.stringify(envelope); const url = await this.getEventsUrl(); return url ? await this.BroadcastEvent(url, body, envelope, skipThrottling) : false; } async BroadcastEvent(url, body, envelope, skipThrottling) { if (await this.shouldBroadcastEvent(body, skipThrottling)) { const res = await fetch(url, { headers: { "Content-Type": "application/json" }, method: "POST", body, cache: "no-store" }); if (res.ok) await this.pushEventToHistory(body); return res.ok; } return false; // already sent in the past hour }}Observed during dynamic analysis: the JSON body of this POST included the same anonymous user ID (b848c461-7053-4619-acf1-7e479320321d) that had just been written to chrome.storage.local under gf_anonymous_user_info, confirming the ID leaves the device rather than staying local to the extension.
- api.givefreely.com
Receives the PUT request that creates the anonymous user profile and issues the persistent bearer token (GiveFreely Inc., an affiliate/cause-marketing network).
- events.givefreely.com
Receives periodic event reports, each carrying the stored anonymous user ID in the request body.
- cdn.givefreely.com
Serves the remote configuration (API base URL, events URL, merchant list) that controls where the above requests are sent.
GiveFreely SDK sends the checkout page URL you're viewing to its servers
On a cart or checkout page, CSS Peeper's GiveFreely SDK sends the full page URL to GiveFreely.
A local hostname check against a cached merchant list runs first; only a matching hostname plus checkout/cart path triggers it.
Confirmed live.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You reach the cart or checkout page of a site on GiveFreely's merchant list.
The content script polls the current page's hostname and URL path against locally cached checkout/cart rules every 2 seconds.
The extension sends the full page URL to GiveFreely's servers.
The request also carries the matched merchant ID, domain ID, and page language, plus the anonymous user ID GiveFreely assigned on install.
Step 1: content script sends only the hostname to the background (local check, no network)
let { domain: activeDomain } = await (async (logger) => { let { hostname } = window.location; logger.debug("Checking if domain is active", { hostname }); let result = await sendMessageToBackground({ type: "GF_IS_ACTIVE_DOMAIN", payload: { hostname } // only the hostname leaves the content script }); return result.payload.isActive ? { isActive: true, domain: result.payload.domain } : { isActive: false, domain: undefined };})(logger);Step 2, once hostname + checkout rules match, the full URL is reported off-device
if (checkoutRulesMatched && !popupAlreadyShown()) { trackEvent(CHECKOUT_POPUP_SHOWN, { url: globalThis.window.location.href, // the full page URL, not just the hostname pageLanguage, merchantId: activeDomain?.merchantId, domainId: activeDomain?.id, provider: activeDomain?.provider, looserCompetingExtensions: listCompetingExtensions(installedExtensions, true) });}Observed during dynamic analysis after navigating to a Best Buy cart page (a merchant on GiveFreely's own active-domains list): the POST body contained url:'https://www.bestbuy.com/cart' along with the merchant/domain identifiers, confirming the exact page URL is transmitted once the merchant + checkout-rule match succeeds.
- Page URLhttps://www.bestbuy.com/cart
The full address of the cart/checkout page you were viewing, including any query parameters it carries.
- Merchant / domain IDmerchantId: 8842, domainId: 341
Identifiers for the matched merchant from GiveFreely's own merchant catalog.
- Page languageen
The detected language of the page you were on.
- Competing extensions[]
Whether other GiveFreely-affiliated extensions were also detected as installed, used to decide which one shows the popup.
- Anonymous user IDb848c461-7053-4619-acf1-7e479320321d
The same persistent ID created on install (see the companion finding), attached automatically to every event including this one.
- events.givefreely.com
Receives the checkout-page-shown event, including the full page URL, when a merchant + checkout-rule match succeeds (GiveFreely Inc., an affiliate/cause-marketing network).
- cdn.givefreely.com
Serves the merchant/domain list and checkout-rule patterns the local hostname match is evaluated against.
CSS Peeper downloads GiveFreely behavior config
CSS Peeper's GiveFreely component downloads partner and global config from cdn.givefreely.com at startup, refreshing every 3,600s.
The server can change popup rules, analytics, affiliate feeds, and charity lists without a store update.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-829
- Source
- Dynamic sandbox
You install or start CSS Peeper.
The extension downloads GiveFreely behavior settings and stores them locally for later use.
Observed storage contained a 1,340-character csspeeperprod partner config after startup; no request body is used for this GET.
Observed storage contained a 9,435-character global config after startup; no request body is used for this GET.
- Refresh intervalconfigRefreshInterval: 3600 seconds
Controls how quickly new server-side settings can replace the copy already on your device.
- Shopping-site rulesmerchantPopupRules entry for trip.com/hotels/booknew
Controls which shopping or checkout pages can trigger GiveFreely popup behavior while you browse.
- Analytics destinationeventsUrl: https://events.givefreely.com/popup
Controls where GiveFreely event records are sent when the extension records popup or affiliate activity.
- Affiliate feed and activation URLsfeedUrl: https://prod.cdnwgf.com/feeds/csspeeperprod.unified-feed.json
Controls which remote feed and activation services the extension can use for shopping offers.
- Charity listUS charity entry with EIN 460858543
Controls which listed charities can appear in the GiveFreely interface.
This shows the downloaded settings were present in the extension after startup, not just referenced in source code.
- Location
- chrome.storage.local GiveFreely config keys
{ "ads_initialized_url": "https://www.amazon.co.uk/", "ads_initialized_timestamp": "1777113928", "GIVE_FREELY_gf_global_config": "9435-character JSON config from cdn.givefreely.com", "GIVE_FREELY_gf_partner_config": "1340-character csspeeperprod config from cdn.givefreely.com", "GIVE_FREELY_gf_last_health_check": "1777113893416", "partner_config.configRefreshInterval": 3600}The shipped background code initializes GiveFreely and fetches both configs
Partner service initialization
static/background/index.jslet u = new i.GiveFreelyService("csspeeperprod");(async () => { try { await u.initialize(); } catch (e) { console.error("Failed to initialize GiveFreely:", e); }})();Config service fetch and merge path
static/background/index.jslet W = "https://cdn.givefreely.com/data/behavioral/";class z { constructor(e) { this.partnerApiKey = e; this.partnerConfig = null; this.globalConfig = null; this.mergedConfig = null; this.globalConfigRefreshInterval = null; this.partnerConfigRefreshInterval = null; } async fetchPartnerConfig() { let e = await w(async () => { let e = await fetch(`${W}${this.partnerApiKey}.json`, { cache: "no-store" }); if (!e.ok) throw Error(`Failed to fetch partner config: ${e.statusText}`); return e.json(); }, this.getRetryOptions()); if (!e) throw Error("Failed to fetch or get from cache partner config"); return this.partnerConfigRefreshInterval = e.configRefreshInterval || this.partnerConfigRefreshInterval, e; } async fetchGlobalConfig() { let e = await w(async () => { let e = await fetch(`${W}global.json`, { cache: "no-store" }); if (!e.ok) throw Error(`Failed to fetch global config => Status code: ${e.status} .${e.statusText}`); return e.json(); }, this.getRetryOptions()); if (!e) throw Error("Failed to fetch or get from cache global config", e); return this.globalConfigRefreshInterval = e.configRefreshInterval || this.globalConfigRefreshInterval, e; } async getConfig(e = false) { this.ensureInitialized(); let t = false; e && (t = await M()); let [r, a] = await Promise.all([ G.get(T, this.fetchPartnerConfig.bind(this), this.partnerConfigRefreshInterval, t, e => e?.configRefreshInterval ?? null), G.get(b, this.fetchGlobalConfig.bind(this), this.globalConfigRefreshInterval, t, e => e?.configRefreshInterval ?? null) ]); if (!a.content) throw Error("Global config is required"); if (!r.content) throw Error("Partner config is required"); this.partnerConfig = r.content; this.globalConfig = a.content; let i = this.mergeConfigs(JSON.parse(JSON.stringify(this.globalConfig)), JSON.parse(JSON.stringify(this.partnerConfig))); if (!i) throw Error("Was not able to merge configs"); this.emitConfigChanges(i); return i; }}- cdn.givefreely.com
Serves partner and global behavior configuration to the extension.
- events.givefreely.com
Named by the downloaded global config as the popup-event endpoint.
- prod.cdnwgf.com
Named by the downloaded partner config as the affiliate-feed host.
- api.givefreely.com
Named by the downloaded configs as the GiveFreely API base URL.