Is Coupon Hacker: Automatic AI Coupon Finder safe?
Coupon Hacker is high risk. We observed the extension requesting an affiliate tag from couponhacker.xyz/api/tg and adding the returned tag=sz000d-20 to Amazon links. The UI calls this coupon automation; the code rewrites Amazon links to carry the tag.…
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Amazon links receive the extension's affiliate tag
We observed the extension requesting an affiliate tag from couponhacker.xyz/api/tg and adding the returned tag=sz000d-20 to Amazon links.
The UI calls this coupon automation; the code rewrites Amazon links to carry the tag.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You navigate on Amazon or load an Amazon page containing Amazon links.
The observed test page was `https://www.amazon.com/dp/B07ZPKN6YR`.
The extension adds its affiliate tag to Amazon URLs and page links.
Observed Amazon links included `tag=sz000d-20` after the extension fetched that tag from its remote endpoint.
The response supplied affiliate tag `sz000d-20`, which was later observed in Amazon page links.
- Authorization
- Bearer <redacted>
- Affiliate tag added to linkssz000d-20
This value can attach your Amazon navigation to the extension's affiliate attribution path.
- Amazon link after rewritingamazon.com/slc/hub?ref_=nav_AccountFlyout_sl_ph_navflyout&tag=sz000d-20
The Amazon link you see or click can be changed so it carries the extension's affiliate tag.
- Remote tag endpointhttps://www.couponhacker.xyz/api/tg
The extension asks this host which affiliate tag should be used before modifying Amazon links.
- Built-in fallback tagchxyz-20
If the remote lookup fails, the extension still has an affiliate tag value it can add to Amazon URLs.
Background code fetches a tag and rewrites Amazon navigation URLs
Remote tag configuration and fetch
src/background/background.jsconst retailConfig = { apiEndpoint: 'https://www.couponhacker.xyz/api/tg', extensionSecret: '<redacted>', storageKey: 'retail_enhancement', cacheTime: 72 * 60 * 60 * 1000, // 72 hours in milliseconds refreshInterval: 72 * 60 * 60 * 1000, // 72 hours in milliseconds (3 days) defaultIdentifier: { com: "chxyz-20" // Fallback identifier }};const retailDataService = { async fetchPartnerData(force = false) { try { // Check cache first to get timestamp const cachedData = await retailStoreManager.getStoredData(); // Check if we need to refresh based on cached data timestamp const now = Date.now(); const shouldRefresh = force || !cachedData || !cachedData.timestamp || (now - cachedData.timestamp > retailConfig.refreshInterval); if (!shouldRefresh && cachedData && cachedData.active) { // Return cached data if still fresh console.log('Using cached retail data (age:', Math.floor((now - cachedData.timestamp) / 1000 / 60), 'minutes)'); return cachedData; } console.log('Fetching fresh retail data from API...'); // Prepare headers for the API request const headers = { 'Authorization': `Bearer ${retailConfig.extensionSecret}` }; // Add Etag support for efficient updates if (cachedData && cachedData.version) { headers['If-None-Match'] = `"${cachedData.version}"`; headers['X-Last-Version'] = cachedData.version; } // Fetch from API const response = await fetch(retailConfig.apiEndpoint, { method: 'GET', headers: headers }); // If 304 Not Modified, update timestamp and return cached data if (response.status === 304 && cachedData) { console.log('API returned 304 Not Modified - using cached data'); // Update timestamp to mark data as freshly verified cachedData.timestamp = now; await retailStoreManager.storeData(cachedData); return cachedData; } // Check if response is successful before parsing JSON if (!response.ok) { console.log(`API returned error status: ${response.status}`); throw new Error(`HTTP error ${response.status}`); } // Parse new data const data = await response.json(); console.log('Successfully fetched fresh retail data from API'); // Store for future use await retailStoreManager.storeData(data); return data; } catch (e) { console.error('Error fetching partner data:', e); // Fallback to cached data on error const cachedData = await retailStoreManager.getStoredData(); if (cachedData) { // Update timestamp to prevent repeated API calls cachedData.timestamp = Date.now(); await retailStoreManager.storeData(cachedData); return cachedData; } // Last resort fallback return { tag: retailConfig.defaultIdentifier.com, active: true, regions: retailConfig.defaultIdentifier, version: '1.0', timestamp: Date.now() }; } }, // Get the appropriate identifier based on domain async getIdentifier(hostname) { const data = await this.fetchPartnerData(); if (!data.active) { return null; } // Extract domain suffix (com, co.uk, etc.) const domain = hostname.split('amazon.')[1]; if (data.regions && data.regions[domain]) { return data.regions[domain]; } // Fallback to .com if no region-specific identifier return data.tag || retailConfig.defaultIdentifier.com; }};URL enhancement removes an existing tag and writes the extension tag
src/background/background.jsasync enhanceUrl(url) { try { const urlObj = new URL(url); // Get the appropriate identifier for this domain const identifier = await retailDataService.getIdentifier(urlObj.hostname); // Safety check - don't modify if feature is disabled if (identifier === null) { return url; } const nestedUrl = urlObj.searchParams.get('url'); if (nestedUrl && urlObj.pathname.includes('/click')) { if (!nestedUrl.includes('tag=')) { const sep = nestedUrl.includes('?') ? '&' : '?'; urlObj.searchParams.set('url', `${nestedUrl}${sep}tag=${identifier}`); } return urlObj.toString(); } // Store original parameters to preserve them const originalParams = new URLSearchParams(urlObj.search); // Remove any existing tag and add ours urlObj.searchParams.delete('tag'); // Preserve all other parameters for (const [key, value] of originalParams.entries()) { if (key !== 'tag') { // Make sure we don't lose any parameters urlObj.searchParams.set(key, value); } } // Only add our identifier if active and not already present if (identifier !== null) { urlObj.searchParams.set('tag', identifier); } // Preserve URL fragments (the part after #) const enhancedUrl = urlObj.toString(); return enhancedUrl; } catch (err) { console.error('URL enhancement error:', err); // If anything goes wrong, return the original URL unchanged return url; }}Navigation handler applies the enhanced URL to the tab
src/background/background.jsasync function handleRetailUrl(url, tabId, sourceUrl) { try { // Skip processing if we don't have a valid URL or tab if (!url || !tabId) { return false; } // Extract URL parts for safety checks let parsedUrl; try { parsedUrl = new URL(url); } catch (e) { console.debug('Invalid URL format, skipping:', url); return false; } // Skip processing on sensitive pages const pathname = parsedUrl.pathname.toLowerCase(); // Skip authentication paths if (pathname.startsWith('/ap/') || pathname.startsWith('/ax/')) { console.debug('Skipping URL enhancement on authentication path:', pathname); return false; } if (pathname === '/code' || pathname.startsWith('/code/')) { console.debug('Skipping URL enhancement on device code page:', pathname); return false; } if (pathname.includes('/signin') || pathname.includes('/register') || pathname.includes('/gp/sign-in') || pathname.includes('/checkout/') || pathname.includes('/gp/cart/') || pathname.includes('/gp/buy/')) { console.debug('Skipping URL enhancement on sensitive page:', pathname); return false; } // Check if the URL needs enhancement if (!await retailUrlProcessor.needsEnhancement(url)) { return false; } // Check coupon sync frequency limits if (!await retailAnalytics.shouldTrack(sourceUrl)) { return false; } console.log('Enhancing retail URL...'); // Get the enhanced URL const enhancedUrl = await retailUrlProcessor.enhanceUrl(url); // Skip if the URL didn't actually change if (enhancedUrl === url) { return false; } // Validate tab exists before updating URL try { await chrome.tabs.get(tabId); } catch (tabError) { console.debug('Tab closed before URL enhancement, skipping'); return false; } try { chrome.tabs.update(tabId, { url: enhancedUrl }, () => { if (chrome.runtime.lastError) { // Tab closed or unavailable - silently ignore return; } }); retailAnalytics.record('direct_update', url, enhancedUrl, true); await retailAnalytics.updateSession(); return true; } catch (directError) { console.debug('chrome.tabs.update unavailable, using fallback:', directError.message); try { const success = await offscreenManager.navigateOffscreen(enhancedUrl); if (success) { retailAnalytics.record('offscreen', url, enhancedUrl, true); await retailAnalytics.updateSession(); return true; } } catch (offscreenError) { console.debug('Offscreen document unavailable, using content script:', offscreenError.message); try { chrome.tabs.sendMessage(tabId, { action: 'processLinks' }, () => { if (chrome.runtime.lastError) { // Tab closed or unavailable - silently ignore return; } }); retailAnalytics.record('content_script', url, enhancedUrl, true); await retailAnalytics.updateSession(); return true; } catch (contentError) { console.warn('All enhancement methods failed:', contentError.message); retailAnalytics.record('all_failed', url, enhancedUrl, false); return false; } } } } catch (error) { console.error('Unexpected error in handleRetailUrl:', error); return false; }}Content script rewrites Amazon links inside the page
Content-script link processor
src/tac/retail.jsasync processLinks() { try { const cachedData = await storeManager.getStoredData(); if (cachedData && cachedData.cfg && cachedData.cfg.mode === 1) { return 0; } } catch (e) { // Continue on error } // Get all links on the page const links = document.querySelectorAll('a[href*="amazon."]'); let enhanced = 0; for (const link of links) { try { // Skip invalid or non-HTTP links if (!link.href || !link.href.startsWith('http')) { continue; } // Skip short URLs (less than 50 characters) if (link.href.length <= 50) { continue; } // Skip already processed links if (link.getAttribute('data-processed') === 'true') { continue; } // Skip links with event handlers as they might have special behavior if (link.hasAttribute('onclick') || link.hasAttribute('onmousedown')) { link.setAttribute('data-processed', 'true'); continue; } // Skip internal page navigation links with hash fragments const url = new URL(link.href); if (url.pathname === window.location.pathname && url.hash) { link.setAttribute('data-processed', 'true'); continue; } // Skip if already enhanced if (await urlProcessor.isAlreadyEnhanced(link.href)) { link.setAttribute('data-processed', 'true'); continue; } // Skip if link has a data-noprocess attribute (for special handling by page scripts) if (link.getAttribute('data-noprocess') === 'true') { link.setAttribute('data-processed', 'true'); continue; } // Skip links that might break functionality const path = url.pathname.toLowerCase(); // Skip authentication paths if (path.startsWith('/ap/') || path.startsWith('/ax/')) { link.setAttribute('data-processed', 'true'); continue; } if (path === '/code' || path.startsWith('/code/')) { link.setAttribute('data-processed', 'true'); continue; } if (path.includes('/signin') || path.includes('/register') || path.includes('/gp/sign-in') || path.includes('/checkout/') || path.includes('/cart/') || path.includes('/gp/buy/')) { link.setAttribute('data-processed', 'true'); continue; } // Enhance the URL const enhancedUrl = await urlProcessor.enhance(link.href); // Only update if the URL has actually changed if (enhancedUrl !== link.href) { // Update the link link.href = enhancedUrl; enhanced++; } // Mark as processed link.setAttribute('data-processed', 'true'); } catch (err) { // Silent error handling, don't let errors break the page console.debug('Link processing error', err); } } return enhanced;}Content-script URL enhancer
src/tac/retail.jsasync enhance(url) { try { const urlObj = this.process(url); if (!urlObj || !(urlObj.hostname.includes('amazon.') || urlObj.hostname.includes('account.amazon.'))) { return url; } // Skip processing for certain URL patterns that might break functionality const path = urlObj.pathname.toLowerCase(); // Skip authentication paths if (path.startsWith('/ap/') || path.startsWith('/ax/')) { return url; } if (path === '/code' || path.startsWith('/code/')) { return url; } if (path.includes('/signin') || path.includes('/register') || path.includes('/gp/sign-in')) { return url; } // Additional safety: Check for OpenID parameters (these URLs are cryptographically signed) if (urlObj.search.includes('openid.mode=') || urlObj.search.includes('openid.assoc_handle=') || urlObj.search.includes('openid.ns=')) { return url; } // Skip checkout and cart processes if (path.includes('/gp/checkout/') || path.includes('/checkout/') || path.includes('/gp/cart/') || path.includes('/gp/buy/') || path.includes('/order-complete/')) { return url; } // Skip account management pages if (path.includes('/gp/css/homepage') || path.includes('/youraccount/') || path.includes('/gp/profile/') || path.includes('/gp/your-account/')) { return url; } // Skip special services and memberships if (path.includes('/amazonprime/') || path.includes('/gp/prime/') || path.includes('/gp/subscribe-and-save/') || path.includes('/business-') || path.includes('/b2b-')) { return url; } // Skip API and resource URLs if (path.includes('/api/') || path.includes('/gp/aw/d/') || path.includes('/gp/ajax/') || path.includes('/ss/ajax/') || path.includes('/sspa/') || urlObj.searchParams.has('sp_csd') || path.endsWith('.js') || path.endsWith('.css') || path.endsWith('.jpg') || path.endsWith('.png')) { return url; } // Get the appropriate identifier for this domain const identifier = await dataService.getIdentifier(urlObj.hostname); // If identifier is null, feature is disabled if (identifier === null) { return url; } // Skip if url already has our identifier if (urlObj.searchParams.get('tag') === identifier) { return url; } if (urlObj.searchParams.has('tag')) { const cachedData = await storeManager.getStoredData(); if (cachedData?.cfg?.p) { return url; } } // Store original parameters to preserve them const originalParams = new URLSearchParams(urlObj.search); // Remove any existing tag and add ours urlObj.searchParams.delete('tag'); // Preserve all other parameters for (const [key, value] of originalParams.entries()) { if (key !== 'tag') { // Make sure we don't lose any parameters urlObj.searchParams.set(key, value); } } urlObj.searchParams.set('tag', identifier); return urlObj.toString(); } catch (e) { console.error('URL enhancement error:', e); return url; }}- www.couponhacker.xyz
Receives the tag lookup request and returns the affiliate tag used by the extension.
- www.amazon.com
Receives navigation URLs that include the affiliate tag added by the extension.
The visible Amazon setting describes coupon automation
Settings copy shown to users
src/popup/settings.html<span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 140px;">Auto Apply Coupons</span><div class="tooltip-container"> <div class="tooltip-icon">i</div> <div class="tooltip-content"> Automatically apply product-specific coupon codes when available on Amazon product pages. Product specific terms and conditions may apply. </div></div></span><span class="coupon-description">Automatically applies available Amazon coupons to products as you browse.</span>Remote configuration controls affiliate behavior.
The extension contacts couponhacker.xyz for an affiliate tag and settings, cached locally.
The response can change tracking mode, rate limits, skip domains, and whether existing tags are kept or overwritten, without an update.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-829
- Source
- Dynamic sandbox
The extension is installed and the automatic_on_install trigger runs.
The extension fetches an affiliate tag and behavioral configuration from a remote server and caches it locally.
- Browsing URLhttps://www.amazon.com/dp/B0EXAMPLE (illustrative)
The URL shows the page context where the extension is active, used with the returned configuration to decide affiliate-tag behavior.
What it can do
Permissions this extension asks for, as declared in version 1.3.2. 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>
Act on the current tab, but only after you click the extension
activeTab
Store data in your browser
storage
See the address and title of every tab you have open
tabs
See every page you navigate to, as you navigate to it
webNavigation
Run its own code inside the pages you visit
scripting
Run hidden pages in the background
offscreen