Is Shop Deals safe?
Shop Deals sends Google search result URLs to Takeads and rewrites eligible results to affiliate links.
When you use Google Search, Shop Deals reads the result links on the page and sends those URLs to Takeads to check for monetization matches. If Takeads returns partner data, the extension changes matching search result links to affiliate tracking URLs and adds a discounts button.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Shop Deals ships a working Takeads API token inside its public code
Shop Deals carries a working Takeads API key in the code every user downloads, so anyone who unpacks the extension can read it.
We extracted that key and used it on its own against the live Takeads API, which accepted the call.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-522
- Source
- Dynamic sandbox
You run a search on google.com with the extension installed.
The content script is scoped to https://www.google.com/* and runs at document_end on every results page.
The extension sends your result links to the Takeads monetization API, proving who it is with an API key that is written into the code every user downloads.
The same key is reused by the popup when it looks up coupons for the page you are on.
The same bearer token authorizes two different Takeads endpoints
What the content script call does
assets/chunk-3db9f861.jsconst body = JSON.stringify({ iris: linkHrefs, // every organic result link on the page subId: partnerId || "common", withImages: true});const response = await fetch( "https://api.takeads.com/v1/product/monetize-api/v2/resolve", { method: "PUT", headers: { "Content-Type": "application/json", // static credential, identical in every copy of the extension Authorization: "Bearer 99dc7ff2<redacted 40-char hex token>" }, body });What the popup call does
assets/chunk-433c5e1b.jsconst body = JSON.stringify({ iris: [activeTab.url], // the page you are currently viewing subId: partnerId});fetch( "https://api.takeads.com/v1/product/monetize-api/v1/coupon/search", { method: "POST", headers: { "Content-Type": "application/json", // same credential as the content script path Authorization: "Bearer 99dc7ff2<redacted 40-char hex token>" }, body }).then(r => r.json());HTTP 201 with an empty data array. The test URL had no matching partner offer, but the API authenticated and accepted the request, which is what shows the key is live.
- Content-Type
- application/json
- Authorization
- Bearer 99dc7ff2<redacted>
{ "iris": [ "https://www.amazon.com/dp/B08N5WRWNW" ], "subId": "common", "withImages": true}- Takeads API keyBearer 99dc7ff2<redacted>
One key shared by every install. It cannot be revoked for a single user without breaking the extension for all of them.
- Links being monetizedhttps://www.amazon.com/dp/B08N5WRWNW
The search result links, or the page you are viewing, that get sent for an affiliate match.
- Partner attribution IDcommon
Identifies which distribution partner gets paid for the traffic. It is fetched at install time and stored.
- Returned tracking linkhttps://tatrck.com/h/0Hu30zkd0RRb?url=https%3A%2F%2Fwww.amazon.com%2Fdp%2FB08N5WRWNW&s=common
The replacement affiliate link the API hands back, which the extension writes over the original result link.
- api.takeads.com
Takeads monetization API. Receives the result links and returns affiliate deeplinks. This is the endpoint the bundled bearer token authenticates to.
- extads.monetize.software
Install attribution service. Read at install time for a partner ID that is then stored and sent as subId on monetization calls.
- tatrck.com
Takeads click redirector. Serves the tracking links returned by the API and written into your search results.
Scans an unpacked extension directory for Authorization: Bearer values written directly into shipped JavaScript, and reports which file and which endpoint each one is attached to.
- Node.js 18 or newer
- an unpacked copy of the extension
#!/usr/bin/env node// Find hardcoded bearer credentials in an unpacked browser extension.// Usage: node find-bundled-bearer-tokens.js /path/to/unpacked-extensionconst fs = require('fs');const path = require('path');const root = process.argv[2];if (!root) { console.error('usage: node find-bundled-bearer-tokens.js <unpacked-extension-dir>'); process.exit(1);}const BEARER = /Bearer\s+([A-Za-z0-9._\-]{16,})/g;const URL_RE = /https?:\/\/[^"'`\s\\]+/g;function walk(dir, out = []) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) walk(full, out); else if (/\.(js|mjs|html|json)$/i.test(entry.name)) out.push(full); } return out;}const findings = new Map();for (const file of walk(root)) { const text = fs.readFileSync(file, 'utf8'); let m; BEARER.lastIndex = 0; while ((m = BEARER.exec(text)) !== null) { const token = m[1]; // nearest URL literal before the header, which is normally the endpoint const before = text.slice(Math.max(0, m.index - 400), m.index); const urls = before.match(URL_RE) || []; const endpoint = urls.length ? urls[urls.length - 1] : '(endpoint not resolved)'; const key = token + '|' + path.relative(root, file) + '|' + endpoint; findings.set(key, { token, file: path.relative(root, file), endpoint }); }}if (findings.size === 0) { console.log('No hardcoded bearer tokens found.'); process.exit(0);}const byToken = new Map();for (const f of findings.values()) { if (!byToken.has(f.token)) byToken.set(f.token, []); byToken.get(f.token).push(f);}for (const [token, uses] of byToken) { const masked = token.slice(0, 8) + '...' + '(' + token.length + ' chars)'; console.log('\nToken ' + masked + ' shipped in the bundle, used ' + uses.length + ' time(s):'); for (const u of uses) { console.log(' ' + u.file + ' -> ' + u.endpoint); } if (uses.length > 1) { console.log(' NOTE: one credential reused across multiple call sites.'); }}- 1Unpack the extension CRX into a directory.
- 2Run: node find-bundled-bearer-tokens.js ./unpacked-extension.
- 3Read the report; each line pairs a shipped token with the endpoint it authenticates to.
What it can do
Permissions this extension asks for, as declared in version 1.3.0. 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
<all_urls>
Store data in your browser
storage
Read and change cookies, including the ones that keep you signed in
cookies
See the address and title of every tab you have open
tabs
Act on the current tab, but only after you click the extension
activeTab
Where it sends data
Destinations our analysis observed Shop Deals contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.
- api.takeads.com
Shop Deals sends data to api.takeads.com. 3 other extensions we have analysed send data here.