Is Similar Sites - Discover Related Websites safe?
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.…
Who publishes itSimilarWeb - no other listings under this identity, 3 shared hostnames
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.
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.
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
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 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.
- Request URLhttps://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 bodyemail=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.
200 response observed for the configuration request; no /numberOfSimilarSites request containing intercepted page data was captured.
{ "sid": "a2dbadf9f"}Fetch, XHR, relay, and reporting paths
Readable fetch replacement
frame_ant/frame_ant.jswindow.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.jst.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.jsasync 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.jstestReallyEqual(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}- 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
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'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 URLhttps://example.com/search?q=benefits+enrollment
This can reveal the site, path, and query values involved in the request.
- Request methodPOST
This distinguishes ordinary page loads from submissions or API calls that send data.
- Request headersAuthorization: Bearer redacted-token
Headers can include cookies, authorization values, language settings, and other browser context.
- Request bodycomment=Please approve purchase order 4821
Submitted form or API data can include text you entered into a page.
Configuration traffic was observed; dynamic analysis also observed webRequest request-body events, but no rule-extracted POST to /numberOfSimilarSites was captured.
webRequest listener registration and all-host permission
Readable body/header listener branch
background/background.jsasm_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": [ "*://*/*"]- 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
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 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.
- WebSocket URLwss://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 markerws
This labels the event as WebSocket traffic so the extension can process it differently from fetch or XHR traffic.
Configuration traffic was observed; no captured request showed a WebSocket message payload going to /numberOfSimilarSites.
WebSocket wrapper and content-script relay
Readable WebSocket replacement
frame_ant/frame_ant.jsif ("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.jsasync 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.jsasync 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))}- 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