Is Behavix safe?

Medium risk

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.

engineeringv1.1.26Chrome Web Store
45Risk

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

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI FOUND

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.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You browse to any website while Behavix's content script runs on every page.

The extension did this

The extension asks Behavix's server for that domain's config and can turn on network and page-content capture with no further prompt.

02EvidencePLAIN NOTE
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.

03EvidenceFIELD TABLE
Fields the server controls per domain, unconstrained in the shipped code
FieldValueWhy it matters
Which page path
path: "/checkout", path_matcher: "STARTS_WITH"The server names the exact page path a capture rule applies to on the matched domain.
Content match pattern
matcher_regex: "\"orderId\":\"(.+?)\""A capture rule only fires when a regex the server sends matches the page or response content.
Send response body
capture_request_url: falseWhen set, the full matched network response body is sent to Behavix, not just the page URL.
Send request body too
capture_request_body: trueWhen set, the data the page itself POSTed is sent alongside the response, not just what came back.
Page element to read
css_selector: ".order-summary"A CSS selector or XPath the server supplies; its HTML or text is read from the page and sent.
04EvidenceCODE COMPARE
The code that does this

The network patch and its server-driven matcher, background.js

What it actually does
fetch()/XHR patch, annotatedbackground.js
// tn(): patches window.fetch so every outgoing request is inspected
function 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, annotatedbackground.js
// we(): decides WHAT gets sent, entirely from server-supplied booleans/regex
function 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,
  };
}
05EvidenceCODE COMPARE
The code that does this

Arbitrary DOM-content capture by server-supplied selector, content.js

What it actually does
// 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;
  }
}
06EvidenceTHIRD PARTY LIST
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.

07EvidenceARTIFACT
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.

RequiresNode.js 18+ (global fetch)A Behavix audience ID from the extension's registration flow
check_behavix_remote_config.js · js
// 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();
How to run it
  1. 1
    Get an audience ID from the extension's own registration link.
  2. 2
    Run: node check_behavix_remote_config.js <audience_id>.
  3. 3
    Read the printed domain -> capture-kind list.
08EvidencePLAIN NOTE
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.

Data recipients

ingest.prod.behavix.io
Updated 20 September 2026mbnfenkapmaacjncnodmkjahemobhnmp