Is Troywell Cashback - discounts and cashback safe?
Troywell Cashback is high risk. We observed Troywell Cashback send a Google Analytics install event with installed extension IDs. The install handler calls the management API, collects every installed extension ID, and puts the array in the event label with the UUID.…
Who publishes it1 other listing from the same operator, 1 of them carrying a finding
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, 500k+ users between them. 1 of them carries a finding.
Shared hosts - 3 hostnames
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.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Installed extension IDs sent to Google Analytics
We observed Troywell Cashback send a Google Analytics install event with installed extension IDs.
The install handler calls the management API, collects every installed extension ID, and puts the array in the event label with the UUID.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You install Troywell Cashback.
The extension lists installed browser extensions and sends their IDs in an analytics event.
Observed during dynamic analysis: body event Management/Installed label=["fopngjlhcjlcadegfhkekadiaephjcaj"]; hooks show chrome.management.getAll.
- Installed extension IDfopngjlhcjlcadegfhkekadiaephjcaj
This identifies another browser extension installed in the same profile.
- Event categoryManagement
This labels the event as information about your browser's installed extensions.
- Event actionInstalled
This marks the extension-list event as part of the install flow.
- Extension UUIDa8g2kqz1n5p7h0m3r9v6c4xw1s2e8d0b9f7j
This lets repeated analytics events from the same extension installation be tied together.
Install-time extension inventory is turned into a GA event
Readable install handler
deobfuscated/bg/bundle.jsasync sendOnInstalled() { await this.loadExtId(), this.settings.isAnalyticsEnabled && (this.sendToGa({ ec: "Extension", ea: "Install" }), chrome.management && chrome.management.getAll((t => { const e = t.map((t => t.id)); this.sendToGa({ ec: "Management", ea: "Installed", el: JSON.stringify(e) }) })))}Readable analytics sender
deobfuscated/bg/bundle.jssendToGa(t) { let { el: e, ec: n, ea: r } = t; if (!this.settings.isAnalyticsEnabled) return; const i = n.replaceAll("-", "_").replaceAll(" ", "_"), a = { name: i }; if (null != r) { const t = { action: r.replaceAll("-", "_").replaceAll(" ", "_"), category: i, engagement_time_msec: Math.floor(1e3 * Math.random()) + 100, session_id: Math.floor(9999999 * Math.random()) + 1, extension_version: chrome.runtime.getManifest().version }; if (null != e) { const n = e.toString().replaceAll("-", "_").replaceAll(" ", "_"); t.label = n } a.params = t } fetch("https://www.google-analytics.com/mp/collect?measurement_id=G-N6BN6GEHEG&api_credential=<redacted>", { method: "POST", body: JSON.stringify({ client_id: this.extId, user_id: this.extId, timestamp_micros: `${Date.now()}000`, events: [a] }) })}- www.google-analytics.com
Receives the Measurement Protocol event named Management with action Installed and a label containing the installed-extension ID array.
Remote config can turn off matching extensions
Troywell Cashback fetches a thanos config from troywell.org and compares server extension-ID lists against your enabled extensions.
If an enabled ID appears in the any-time or activation-time list, it calls the management API to disable it.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-506
- Source
- Dynamic sandbox
You install Troywell Cashback or activate cashback on a merchant site.
The extension compares your enabled extension IDs with a server list and can turn off matches.
- Any-time extension listfopngjlhcjlcadegfhkekadiaephjcaj
A server-provided ID in this list can be matched against extensions enabled in your browser during startup checks.
- Activation-time extension listcjpalhdlnbpafiamejdnhcphjbkeiagm
A server-provided ID in this list can be matched when you activate cashback.
- Enabled extension IDscjpalhdlnbpafiamejdnhcphjbkeiagm
This reflects which browser extensions are currently enabled in the same profile.
- Enabled flagfalse
This is the value passed to the browser when a matching extension is turned off.
Remote list intersection leads to chrome.management.setEnabled
Readable remote config registration
deobfuscated/bg/bundle.jsconst e = "troywell.org", r = `https://${e}`, S = `${r}/api/configs/thanos`;class Mt extends yt { constructor() { super({ name: "configs.thanos", url: S, options: { updateIntervalMinutes: 240 } }) } parse(t) { return t[0] || {} }}Readable extension-disabling controller
deobfuscated/bg/bundle.jsclass Ft { constructor() { this.name = "thanos", this.browser = ot(), this.enabled = !1, this.allExt = [] } async init() { this.config = n.g.app.configs.thanos, this.controller(!1, "anyTime") } async controller(e) { let n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : "activationTime"; if (this.checkState(), !this.enabled) return; e || (this.allExt = await this.getAllEnabledExtension()); (0, t.intersection)(this.config?.data[n] || [], this.allExt).forEach((t => { this.toggleExtension(t, e) })) } async getAllEnabledExtension() { return new Promise((t => { try { chrome.management.getAll && chrome.management.getAll((e => { const n = e.filter((t => { let { enabled: e } = t; return e })); t(n.map((t => { let { id: e } = t; return e }))) })) } catch (t) {} })) } toggleExtension(t) { let e = !(arguments.length > 1 && void 0 !== arguments[1]) || arguments[1]; try { if (t === chrome.runtime.id) return; chrome.management.setEnabled(t, e) } catch (t) {} } checkState() { this.enabled = "ya" !== this.browser && "ff" !== this.browser && !!chrome.management }}- troywell.org
Serves the /api/configs/thanos response whose anyTime and activationTime lists are compared with enabled extension IDs.
Coupon checkout details sent to CityAds
When coupon auto-apply runs at checkout, the extension builds an analytics payload and posts it to CityAds.
It can include merchant name/ID, checkout host, coupon code, cart total, savings, extension ID/version, CAA config ID; untested.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You use the coupon auto-apply flow on a merchant checkout page.
The extension builds a checkout analytics payload and sends it to CityAds.
The code sends state changes such as show, close, start, cancel, success, and fail.
- Coupon flow stateSUCCESS
This describes where you are in the coupon auto-apply process, such as starting, canceling, or completing it.
- Checkout hostnamecheckout.example-store.com (illustrative)
This identifies the shopping site where the coupon flow is running.
- Coupon codeSAVE10 (illustrative)
On a successful coupon application, this records which coupon was applied during your checkout session.
- Checkout total59.99 (illustrative)
This gives the analytics endpoint context about the value of the cart where the coupon flow ran.
- Savings amount10.00 (illustrative)
This records how much the coupon flow reports saving during the checkout session.
- MerchantExample Store / 12345 (illustrative)
This identifies the store associated with the checkout page and coupon flow.
- Extension installjcmbmadioaobdgfbmbpenhlcppfcahhi / 1.0.1
This identifies which browser extension build generated the coupon telemetry.
- CAA configurationconfig.id=98765; type=caa (illustrative)
This links the checkout event to the coupon auto-apply configuration used on the page.
Coupon state changes become a CityAds analytics POST
Content-script payload builder
content/bundle.js_ = t => { const n = Fp().getCurrentStates(); let r = null; if ("Show" === t || "Close" === t || "Start" === t || "Cancel" === t || "Success" === t || "Fail" === t) { r = { category: t.toUpperCase(), type: "caa", url: window.location.hostname, caa: { version: Fp().version, coupon: "Success" === t ? u.value.bestCoupon?.code : void 0 }, price: { saved: "Success" === t ? Number.parseFloat(u.value.caaSavings.toFixed(2)) : void 0, total: n.total }, button: { id: chrome.runtime.id, version: chrome.runtime.getManifest().version }, store: { name: s.value.name, id: s.value.id }, config: { id: n.config.id, type: "caa" } }; const o = { ec: "CAA", ea: t, el: s.value.domain }; e.dispatch("sendCaaGA", { ga: o, caaAnalytics: r }) }}Apply and close trigger CAA events
content/bundle.jsapply: async () => { u.value = null, d.value = [], e.dispatch("sendAnalytics", { ea: "CAA_apply_clicked", ec: "CAA", el: g.value }), _("Start"), await Fp().process()},close: async () => { const t = Fp().getCurrentStates(); [Td].includes(t.status) && (c.value = !1, e.dispatch("sendAnalytics", { ea: "CAA_closed", ec: "CAA", el: g.value }), _("Close")), [Od, Ed, jd].includes(t.status) && (await Fp().stop(), Fp().startObserving(), e.dispatch("sendAnalytics", { ea: "CAA_canceled", ec: "CAA", el: g.value }), _("Cancel")), [Nd, Fd].includes(t.status) && (c.value = !1, Fp().startObserving()), y()}Background POST to CityAds
bg/bundle.jssendCaa(t) { let { data: e } = t; if (!this.settings.isAnalyticsEnabled) return; const { caaAnalytics: n } = e; n && fetch("https://cityads.com/mobilerewards/analytics/activity/troywell", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(n) })}- cityads.com
Receives coupon auto-apply analytics containing merchant, checkout, coupon, price, savings, extension, and configuration fields.
+1 more finding not shown
What it can do
Permissions this extension asks for, as declared in version 1.0.1. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.
Read and change your data on every site you visit
*://*/*
Store data in your browser
storage
Store an unlimited amount of data in your browser
unlimitedStorage
See, disable and uninstall your other extensions, including your security ones
management
Run its own code inside the pages you visit
scripting
Act on the current tab, but only after you click the extension
activeTab
See the address and title of every tab you have open
tabs
Read and change cookies, including the ones that keep you signed in
cookies
Block and redirect the requests your browser makes
declarativeNetRequest
See which of your requests its blocking rules matched
declarativeNetRequestFeedback
Schedule its own background tasks
alarms
See every page you navigate to, as you navigate to it
webNavigation
Show you desktop notifications
notifications