Is Behavix safe?
Behavix fetches a server-controlled config per site that can capture network traffic and DOM content, then uploads it to its own servers.
Behavix injects a content script into every website you visit and fetches a per-domain configuration from its own backend that names which network requests to watch and which page elements to read. Depending on what the server sends, it can monkey-patch fetch/XHR to capture response bodies and the data your browser POSTs, or read the innerHTML/innerText of DOM elements matched by a server-supplied CSS selector or XPath, including continuously via a MutationObserver. Matches are relayed from the page to the extension's background script and uploaded to Behavix's ingest endpoint; the extension's install-consent screen describes only browsing history and ad impressions on a few named sites, not this broader, remotely configurable capture.
Who publishes itBehavix Oy - no other listings under this identity
Behavix Oy - no other listings under this identity
What this publisher told the store about itself, and the other listings that told it the same thing.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Behavix says it tracks page visits and ad views. The code can capture much more.
Behavix's consent screen says it tracks only page visits and ad views on Instagram, Facebook and Youtube, adding "that's all it collects." A server-issued config can also read network bodies and page HTML on any site.
What actually happens
You browse to any website while Behavix's content script runs on every page.
The extension asks Behavix's server for that domain's config and can turn on network and page-content capture with no further prompt.
What the opt-in screen tells users
Before opt-in, Behavix tells users it tracks "websites you visit" and ad views on Instagram, Facebook and Youtube, adding "that's all it collects." The remote-config scraper described below is not limited to those sites or to ad content.
Fields the server controls per domain, unconstrained in the shipped code
- Which page pathpath: "/checkout", path_matcher: "STARTS_WITH"
The server names the exact page path a capture rule applies to on the matched domain.
- Content match patternmatcher_regex: "\"orderId\":\"(.+?)\""
A capture rule only fires when a regex the server sends matches the page or response content.
- Send response bodycapture_request_url: false
When set, the full matched network response body is sent to Behavix, not just the page URL.
- Send request body toocapture_request_body: true
When set, the data the page itself POSTed is sent alongside the response, not just what came back.
- Page element to readcss_selector: ".order-summary"
A CSS selector or XPath the server supplies; its HTML or text is read from the page and sent.
The code that does this
The network patch and its server-driven matcher, background.js
fetch()/XHR patch, annotated
background.js// tn(): patches window.fetch so every outgoing request is inspectedfunction patchFetch() { const originalFetch = window.fetch; window.fetch = async function (...args) { const requestArg = args[0]; let url, method; if (typeof requestArg === "string") { url = requestArg; } else if (requestArg instanceof Request) { url = requestArg.url; method = requestArg.method; } else if (requestArg instanceof URL) { url = requestArg.toString(); } const response = await originalFetch.apply(this, args); if (!url) return response; const init = args[1] || {}; const httpMethod = (init.method ?? method ?? "GET").toUpperCase(); // findMatchingRule(): is there a server-supplied rule for this path/method? if (!findMatchingRule(url, httpMethod, activeConfigs)) return response; const responseBodyText = await response.clone().text(); const { reqBody, reqBodyKind } = splitRequestBody(init.body); // buildCapturePayload(): decide (per server config) what to send const capture = buildCapturePayload( { url, method: httpMethod, reqBody, reqBodyKind, resBody: responseBodyText }, activeConfigs, { pageUrl: window.location.href, configVersionHash: currentConfigHash } ); if (capture) relayToBackground(capture); return response; };}Response matcher, annotated
background.js// we(): decides WHAT gets sent, entirely from server-supplied booleans/regexfunction buildCapturePayload(captured, configs, ctx) { const rule = findMatchingRule(captured.url, captured.method, configs); if (!rule) return null; const bodyRegex = rule.config.compiledRegex; // built from server's matcher_regex if (!bodyRegex || captured.resBody === null || !bodyRegex.test(captured.resBody)) { return null; } const hasRequestBody = captured.reqBodyKind === "form-ish" || (captured.reqBodyKind === "string" && captured.reqBody); let payload; if (rule.config.capture_request_url === true) { payload = captured.url; // just the URL } else if (rule.config.capture_request_body === true && hasRequestBody) { // BOTH what the page posted AND what it got back payload = JSON.stringify({ response: captured.resBody, request: extractRequestBody(captured), }); } else { payload = captured.resBody; // the full response body } return { config_id: rule.config.id, config_version_hash: ctx.configVersionHash, payload, url: ctx.pageUrl, };}The code that does this
Arbitrary DOM-content capture by server-supplied selector, content.js
// registerDomPageLoad() + captureNode(): reads arbitrary page content// picked by a server-supplied CSS selector or XPath, gated by a// server-supplied regex over that element's own content.async registerDomPageLoad(rule, configVersionHash) { const cfg = new DomPageLoadRule(rule); if (!pathMatches(cfg.config.path, cfg.config.path_matcher)) return; const findTargets = cfg.config.target_node_xpath !== undefined ? () => evaluateXPath(cfg.config.target_node_xpath) : () => { const nodes = document.querySelectorAll(cfg.config.css_selector); return nodes.length ? Array.from(nodes) : undefined; }; const targets = await pollUntilFound(10, findTargets); targets?.forEach((node) => { if (node instanceof HTMLElement && contentMatchesServerRegex(node.innerHTML, cfg.config)) { this.captureNode(node, cfg.config, configVersionHash); } });}captureNode(node, config, configVersionHash) { if (node.textContent === null) return; switch (config.capture_mode) { case "HTML": this.onMatchedConfig(config.id, node.innerHTML, configVersionHash); break; case "TEXT": if (node.innerText !== null) { this.onMatchedConfig(config.id, node.innerText, configVersionHash); } break; }}Third-party destinations
- *-ingest.prod.behavix.io
Serves the remote config and receives matched captures. The subdomain prefix is the installer's own audience ID, assigned at opt-in.
Check if you're affected
Given a Behavix audience ID, fetches the live remote config and lists which domains currently have network-capture or DOM-capture rules turned on.
- Node.js 18+ (global fetch)
- A Behavix audience ID from the extension's registration flow
// check_behavix_remote_config.js//// Reads the same per-domain scraping config Behavix's own extension fetches,// so you can see which sites currently have a network-capture or DOM-capture// rule active for a given audience ID, without installing the extension.//// Usage: node check_behavix_remote_config.js <audience_id>const audienceId = process.argv[2];if (!audienceId) { console.error("Usage: node check_behavix_remote_config.js <audience_id>"); process.exit(1);}const host = `${audienceId}-ingest.prod.behavix.io`;async function main() { const res = await fetch(`https://${host}/remote-config`, { method: "GET", headers: { "x-api-key": audienceId }, }); if (!res.ok) { console.error(`Request failed: HTTP ${res.status}`); process.exit(1); } const configs = await res.json(); console.log(`Rules returned for audience ${audienceId}:\n`); for (const rule of configs) { const kinds = []; if (rule.MONKEY_PATCH?.length) kinds.push(`MONKEY_PATCH(${rule.MONKEY_PATCH.length})`); if (rule.DOM_PAGE_LOAD?.length) kinds.push(`DOM_PAGE_LOAD(${rule.DOM_PAGE_LOAD.length})`); if (rule.DOM_CHANGE_LISTENER?.length) kinds.push(`DOM_CHANGE_LISTENER(${rule.DOM_CHANGE_LISTENER.length})`); if (kinds.length) { console.log(` ${rule.domain} -> ${kinds.join(", ")}`); } }}main();- 1Get an audience ID from the extension's own registration link.
- 2Run: node check_behavix_remote_config.js <audience_id>.
- 3Read the printed domain -> capture-kind list.
Observation
Static analysis finding. This behaviour was identified by reading the shipped extension code and has not yet been reproduced in a live run. The trigger conditions and the exact data sent are read from the code, not from an observed capture.
Where it sends data
Destinations our analysis observed Behavix contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.
- ingest.prod.behavix.io
Behavix sends data to ingest.prod.behavix.io. No other extension we have analyzed sends data here.
