Is FBA calculator for Amazon Sellers : SellerApp safe?

Medium risk

fba calculator is medium risk. The extension opens a WebSocket to SellerApp's scrapernet service with query fields for country, extension type, IP, ZIP, and Amazon login state. The source shows it can also receive scraping jobs and fetch pages with browser credentials.

sellerapp-devv4.0.3Chrome Web Store
45Risk
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, 66k+ 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

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

SellerApp WebSocket sends Amazon session context

The extension opens a WebSocket to SellerApp's scrapernet service with query fields for country, extension type, IP, ZIP, and Amazon login state.

The source shows it can also receive scraping jobs and fetch pages with browser credentials.

01EvidenceCAUSE EFFECT
What actually happens
You did this

The extension background script starts after installation or browser startup.

The extension did this

It opens a persistent connection to SellerApp and attaches browser and Amazon-session context to the connection URL.

02EvidenceNETWORK CAPTURE
Captured request
GETwss://scrapernet.sellerapp.com/scraper-net/websocket?geo=GB&ext_type=fba-calc&ip_address=undefined&zip_code=undefined&is_user_logged_in=true
WebSocket connection opened; five health messages followed.
03EvidenceFIELD TABLE
Fields attached to the SellerApp WebSocket connection
FieldValueWhy it matters
Country
geo=GBThis tells the remote service which marketplace or geography your browser is being associated with.
Network address field
ip_address=undefinedWhen populated, this can identify the network your browser is using; in the captured run the field was sent with an undefined value.
ZIP-code field
zip_code=undefinedCan reveal a more specific delivery/shopping region when Amazon returns one; in the captured run it was sent as undefined.
Amazon login state
is_user_logged_in=trueThis tells the remote service whether the browser appeared to be signed in to Amazon during the check.
Extension mode
ext_type=fba-calcThis identifies the extension workflow using the socket.
04EvidenceCODE COMPARE
The code that does this

Socket startup, health frames, and server-sent Amazon scraping jobs

What it actually does
Readable startup builds the WebSocket URLdeobfuscated/background.bundle.js
        t.initializeSocket = (e, t) => {
          (0, o.get)("socket_id").then((e => {
            T = e
          })).catch((() => {
            T = (0, a.v4)(), (0, o.set)("socket_id", T).then((() => {}))
          })).finally((() => r(void 0, void 0, void 0, (function*() {
            var n;
            try {
              E = yield(0, o.get)("country_id"), g = yield(0, o.get)("ip_address")
            } catch (e) {
              const t = yield fetch("https://lumtest.com/myip.json"), n = yield t.json();
              E = n.country, g = n.ip, (0, o.set)("country_id", E), (0, o.set)("ip_address", g)
            }
            let r = "";
            try {
              r = yield(0, s.fetchDetails)(h[E])
            } catch (e) {}
            if (r) {
              y = (0, s.parse)(r, i.zipCodeSchema);
              const e = (0, s.parse)(r, i.userLoggedInSchema);
              b = !(null === (n = null == e ? void 0 : e.sign_in_button_url) || void 0 === n ? void 0 : n.includes("signin"))
            } else y = {
              zip_code: ""
            };
            f = (0, a.v4)(), p = new WebSocket(`wss://scrapernet.sellerapp.com/scraper-net/websocket?id=${T}&instance_id=${f}&geo=${E}&ext_type=${e}&account_id=${t}&ip_address=${g}&zip_code=${null==y?void 0:y.zip_code}&is_user_logged_in=${b}`), w(p, T, e)
          }))))
        };
Readable handlers keep the socket alive and process jobsdeobfuscated/background.bundle.js
        const w = (e, n, i) => {
            e.onopen = () => r(void 0, void 0, void 0, (function*() {
              console.log("WebSocket connection established........"), D = 1e3, M = 6e4, L = 100, A && clearInterval(A), A = setInterval((() => {
                e.send(JSON.stringify({
                  socket_id: n,
                  status: 200,
                  type: "health",
                  zip_code: null == y ? void 0 : y.zip_code
                }))
              }), 3e4);
              try {
                const t = yield Y();
                e.send(JSON.stringify({
                  socket_id: n,
                  status: 200,
                  type: "login_status",
                  login_status: t
                }))
              } catch (e) {
                console.log("Error in checking multiple countries login")
              }
            })), e.onmessage = function(i) {
              return r(this, void 0, void 0, (function*() {
                const o = i.data,
                  u = JSON.parse(o);
                if (u.url || "RUFUS_EXTENDED" === u.parser_config_url.page_type) {
                  let i = u.url;
                  const o = u.parser_config_url.page_type;
                  if ("RUFUS_EXTENDED" === o) {
                    const e = h[u.parser_config_url.geo] || "https://www.amazon.in";
                    if (i = `${e}/rufus/cl/streaming?tabId=${u.tabId||(0,a.v4)()}&ref=nl_cl_dsk_csq&programId=NILE_CLASSIC:desktop-cl`, !u.rufusToken) {
                      const n = u.url || e,
                        r = yield(0, s.fetchDetails)(n);
                      "string" == typeof r && (u.rufusToken = yield(0, t.getRufusToken)(r, n))
                    }
                  }
                  if (u.zipcode && i) {
                    const e = /https?:\/\/(www\.)?([a-zA-Z0-9.-]+)/,
                      t = c.default.parseUrl(i).url.match(e);
                    if (t) {
                      const e = t[0],
                        n = yield(0, s.fetchDetails)(e);
                      yield(0, l.setZipCode)(n, e, u.zipcode)
                    }
                  }
                  const d = "hHqVzDHmU7Is8nzCo2Y2worrxE6Le8i4eNuimVauLTHwAAAAAGmv31piMDI3OTI3Mi0xN2VjLTRkM2YtYjI5Yi0yMjlkMjViMmJjOGM=",
                    m = "RUFUS_EXTENDED" === o ? {
                      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": d,
                        Accept: "*/*",
                        "Cache-Control": "no-cache",
                        "sec-fetch-dest": "empty",
                        "sec-fetch-mode": "cors",
                        "sec-fetch-site": "same-origin"
                      },
                      isStream: !0
                    } : {};
                  (0, s.fetchDetails)(i, !1, 1, 500, m).then((a => r(this, void 0, void 0, (function*() {
                    let r = {};
                    "object" == typeof a && 200 !== a.status ? r = {
                      socket_id: n,
                      data: {},
                      timestamp: new Date,
                      status: a.status,
                      scrapeUrl: i,
                      instance_id: f,
                      version: _.version,
                      html: "",
                      request_id: u.request_id
                    } : "string" == typeof a && ("RUFUS_EXTENDED" === o ? r = yield(0, t.getRufusExtendedDetails)(a, u.url, u.request_id): "KEYWORD_SEARCH" === o ? r = yield I(a, i, u.parse_html, u.all_pages || !1, (null == u ? void 0 : u.page_limit) || 7, u.request_id): "PRODUCT_LISTING" === o ? r = C(a, i, u.parse_html, u.request_id) : "SELLER_PROFILE" === o ? r = k(a, i, u.parse_html, u.request_id) : "QNA_DETAILS" === o ? r = O(a, i, u.parse_html, u.request_id) : "REVIEW_DETAILS" === o ? r = yield v(a, i, u.parse_html, u.request_id): "BEST_SELLER" === o ? r = R(a, i, u.parse_html, u.request_id) : "OFFERS_DETAILS" === o ? r = yield P(a, i, u.parse_html, u.request_id): "RUFUS_DETAILS" === o && (r = yield(0, t.getRufusDetails)(a, u.url, u.query, u.request_id, u.searchSource))), e.send(JSON.stringify(r))
                  })))).catch((t => {
                    e.send(JSON.stringify({
                      error: "parse error",
                      request_id: u.request_id,
                      status: 500
                    }))
                  }))
                }
              }))
            }, e.onerror = function(e) {
              A && clearInterval(A), console.error("WebSocket error:", e)
            }, e.onclose = function(n) {
              A && clearInterval(A),
                function(n) {
                  e && e.readyState !== WebSocket.CLOSED || (D < M && (D *= 2), L > 0 ? (console.log(`Attempting to reconnect in ${D} ms. Attempts left: ${L}`), setTimeout((function() {
                    (0, t.initializeSocket)(n, S), L--
                  }), D)) : console.error("Maximum reconnection attempts reached. Unable to establish WebSocket connection."))
                }(i)
            }
          },
Fetch helper includes browser credentials when retrieving pagesdeobfuscated/background.bundle.js
        t.fetchDetails = (e, t = !1, n = 1, a = 500, s = {}) => r(void 0, void 0, void 0, (function*() {
          var r, i;
          let o = 0;
          for (; o <= n;) try {
            const c = yield fetch(e, {
              method: s.method || "GET",
              mode: "cors",
              credentials: "include",
              headers: s.headers || {},
              body: s.body ? JSON.stringify(s.body) : void 0
            });
            if (c.url.includes("ap/signin") && (o = n), !c.redirected || !t || o === n) {
              if (o === n && 200 !== c.status) return {
                status: 302
              };
              if (200 === c.status) {
                if (s.isStream) {
                  if (!c.body) throw new Error("ReadableStream not supported in response");
                  const e = c.body.getReader(),
                    t = new TextDecoder("utf-8");
                  let n = "",
                    a = "";
                  for (;;) {
                    const {
                      value: s,
                      done: i
                    } = yield e.read();
                    if (i) break;
                    a += t.decode(s, {
                      stream: !0
                    });
                    const o = a.split("\n");
                    a = o.pop() || "";
                    for (const e of o) {
                      if (!e.startsWith("data:")) continue;
                      const t = e.replace(/^data:\s*/, "").trim();
                      if (t) try {
                        n += null !== (r = JSON.parse(t).htmlRenderedChunk) && void 0 !== r ? r : ""
                      } catch (e) {
                        console.error("❌ Invalid JSON chunk:", e.message)
                      }
                    }
                  }
                  if (a.startsWith("data:")) {
                    const e = a.replace(/^data:\s*/, "").trim();
                    if (e) try {
                      n += null !== (i = JSON.parse(e).htmlRenderedChunk) && void 0 !== i ? i : ""
                    } catch (e) {
                      console.error("❌ Invalid JSON chunk:", e.message)
                    }
                  }
                  return n
                }
                return yield c.text()
              }
              return {
                status: c.status
              }
            }
            o++, yield new Promise((e => setTimeout(e, a * Math.pow(2, o))))
          } catch (e) {
            return {
              status: 500
            }
          }
Background startup invokes the SellerApp socketdeobfuscated/background.bundle.js
    })), (0, zo.initializeSocket)("fba-calc", ""), chrome.runtime.onUpdateAvailable.addListener((function(e) {
      console.log(e.version), chrome.runtime.reload()
    }))
05EvidenceTHIRD PARTY LIST
Remote services involved in this flow
  • scrapernet.sellerapp.com

    SellerApp scrapernet WebSocket endpoint receiving connection metadata, health frames, login-status results, and parsed scraping results.

  • lumtest.com

    IP geolocation lookup used by the extension when country and IP address are not already stored locally.

  • amazon.com

    Amazon marketplace pages are fetched by the extension when the socket job path requests Amazon page data.

Updated 30 September 2026bkdkbhjcfhfkmkbffkdklaiepfbllbgg