Is Browse.live Web Safety safe?

High risk

Browse.live Web Safety can scrape configured page text or attributes and send them with page URLs to Browse.live.

Browse.live Web Safety runs on all websites and reports navigation telemetry including full page URLs and referrers. Its server-provided report rules can also make the content script read matching page text or element attributes and send those values with the page URL. The extension also periodically sends Browse.live a list of installed extensions.

AppEsteem Corporationv2.25.1729.2103Chrome Web Store
78Risk
Who publishes it

AppEsteem Corporation - 1 other listing from the same operator, none carrying a finding

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

Publisher
AppEsteem Corporation
Declared legal entity
AppEsteem Corporation
Registered address
655 156th Ave SE STE 275, Suite 275, Bellevue, WA 98007-5022, US
Registered contact
Dennis Batchelder

Same store account

1 other listing published from this account, 4k+ users between them, none of them carrying a finding.

Shared hosts - 6 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.

anonymous.browse.live
Also called by 2 other listings, including Browse.live Web Safety
api.blur.live
Also called by 2 other listings, including Browse.live Web Safety
browse.live
Also called by 2 other listings
customer.appesteem.com
Also called by 2 other listings, including Browse.live Web Safety
staging.browse.live
Also called by 2 other listings, including Browse.live Web Safety
blur.live
Also called by 4 other listings

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

Publishers can request a review.

Findings

Server-controlled rules already flag password fields on any site

The extension checks every page you load against selector rules from its own server.

We confirmed a live rule matches password fields; visiting real login pages made it report the site and rule ID to Browse.live.

Severity
High unwanted
Type
Unexpected
CWE
CWE-359
Source
Dynamic sandbox
What actually happens
You did this

You open a real login page, such as github.com/login, that has a password field.

The extension did this

The extension matches the page against a password-field rule pulled from its own server and reports the match to Browse.live.

The report names the site you visited and the internal rule ID that matched.

What left the browser on each match
  • Site you visited
    github.com

    The exact hostname is sent so the vendor knows which pages have password fields.

  • Which rule matched
    ABIRR-1001

    An internal ID lets the vendor's server track this exact detection pattern across every visit.

  • Number of matches
    1

    How many password fields the page contained.

Captured request
POSThttps://anonymous.browse.live/blur/telemetry/batch
Body
[  {    "TelemetryReason": "PageRuleImpression",    "PageUrl": "https://github.com/login",    "PageUrlClassificationType": "[{\"f\":\"github.com\",\"r\":\"ABIRR-1001\",\"c\":1}]"  }]
The code that does this

The count-only path that fired in our test

Readable version
// b = the per-page rule counter (content script "counter.js")// b.rules = the identificationRules array served by GET /blur/configstatic async reportRuleMatchCounts() {  if (adBlurCount === 0 && deceptorBlurCount === 0 &&      searchIdentifiedCount === 0 && reportRuleCount === 0) return;  const matchCounts = {};  // Ad-blocking rules: count elements matching each rule's CSS selector  for (const rule of rules) {    if (!["ad", "ad-shadowroot", "ad-backdrop"].includes(rule.ruleType)) continue;    if (rule.blurDistance !== 0 || !rule.srcSelector) continue;    const count = document.querySelectorAll(rule.srcSelector).length;    if (count > 0) matchCounts[rule.ruleId] = count;  }  // "report" rules -- e.g. ABIRR-1001, srcSelector "input[type=password]" --  // are counted the SAME way: only how many elements match, not their content.  for (const rule of rules) {    if (rule.ruleType !== "report") continue;    const count = countMatchingElements(document, rule.srcSelector);    if (count > 0) matchCounts[rule.ruleId] = count;  }  // ...other counters omitted...  const delta = diffAgainstLastSentCounts(matchCounts);  if (Object.keys(delta).length > 0) {    await MessageBackend.ReportImpression(delta); // -> background.js -> telemetry POST  }}
The code that does this

A second code path can upload the matched field's actual value

Readable version
// S.rules = the same server-supplied identificationRules, filtered to// ruleType === "report". This runs in a SEPARATE injected script// (reportRuleData.js) from the counting path above.static findMatchingRuleValues(root) {  const valuesByRuleId = {};  // ...recurse into shadow roots (omitted here for brevity, same pattern)...  for (const rule of reportRules) {    // Only rules that name BOTH a selector and an attribute/field to read    // reach this branch -- this is where actual field CONTENT gets pulled.    if (rule.ruleType !== "report" || !rule.srcSelector || !rule.srcAttribute) continue;    const matches = root.querySelectorAll(rule.srcSelector);    for (const el of matches) {      let value = "";      try {        value = rule.srcAttribute.toLowerCase() === "innertext"          ? el.innerText          : el.getAttribute(rule.srcAttribute);      } catch (e) { value = e.message || "codeError"; }      if (value || value === 0) {        valuesByRuleId[rule.ruleId] = valuesByRuleId[rule.ruleId] || [];        valuesByRuleId[rule.ruleId].push(value); // the scraped text/attribute value      }    }  }  return valuesByRuleId; // -> sent via the ReportRuleData message}
Third-party destinations
    • anonymous.browse.live

    Receives per-page rule-match reports and full telemetry batches. Operated by Browse.live (Blur), the extension's own vendor, not an unrelated third party.

What we observed vs. what the code supports

We observed a rule ID and match count leave the browser, not literal field text. The same rule format can also read a field's value; that path exists in code but was not switched on during our test.

Full page URL and referrer sent to Browse.live on every site you visit

Loading any webpage triggers an unprompted report to the extension's own server with the full page URL, referrer, browser version, and locale.

We confirmed this live on an ordinary Amazon.com visit unrelated to ad-blocking.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-359
Source
Dynamic sandbox
What actually happens
You did this

You navigate to any ordinary website, for example amazon.com.

The extension did this

The extension immediately reports the full page URL to its own telemetry server.

The report also includes the page's referrer, your browser's version, and locale.

Fields sent on every page you load
  • Page URL
    https://www.amazon.com/gp/cart/view.html

    The exact address of every page you visit, including query parameters that can carry search terms or account details.

  • Referrer URL
    https://www.google.com/search?q=best+laptop+deals

    The page you came from, revealing your browsing path.

  • Browser identity string
    Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/128.0.0.0

    Your exact browser version and OS, which combined with other data can help fingerprint your device.

  • Language / locale
    en-US

    Your browser's language setting.

Captured request
POSThttps://anonymous.browse.live/blur/telemetry/batch
Body
[  {    "TelemetryReason": "Browse",    "PageUrl": "https://www.amazon.com/",    "RootUrl": "https://www.amazon.com/",    "IsFrame": false  }]
The code that does this

The consent check that never blocks sending

Readable version
// Fires on every top-frame navigation (background.js message handler// for "ContentInjected", called from content.js on every visible page)static async reportBrowseEvent(msg, sender) {  const reason = "Browse";  if (!await shouldSampleThisReason(reason)) return; // sampling rate, not a consent gate  const record = {    TelemetryReason: reason,    PageUrl: sender.url,    RootUrl: sender.tab?.url,    ReferrerUrl: msg.referrerUrl,    IsFrame: sender.frameId !== 0,  };  queueTelemetry(record); // merged with device/browser fields, then POSTed}// This function exists and checks the user's local "send telemetry" flag...static async isTelemetryEnabledInLocalConfig() {  const cfg = await getLocalConfig();  return "sendTelemetry" in cfg && cfg.sendTelemetry === true;}// ...but nothing in the codebase calls it. The function that IS called// before every send ignores the flag entirely:class ConsentGate {  static async canSendTelemetry() {    return true; // hardcoded -- no config, no user action, ever blocks this  }}static async sendBatchTelemetry(batch) {  if (!await ConsentGate.canSendTelemetry()) return "notconsented"; // never hit  return await httpPost(telemetryBatchUrl(), batch);}
Third-party destinations
    • anonymous.browse.live

    Receives a report of every page you visit, including pages the ad-blocking feature never touches. Operated by Browse.live (Blur), the extension's own vendor.

Your full list of installed extensions is uploaded to Browse.live

On install, and again roughly every two hours, the extension asks Chrome for every extension you have installed and uploads each one's ID, version, and permissions to Browse.live's server.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-359
Source
Dynamic sandbox
What actually happens
You did this

You install the extension, or two hours pass while it keeps running.

The extension did this

It asks Chrome for your complete list of installed extensions and uploads it to Browse.live.

Each entry includes that extension's ID, version, and requested permissions.

What's uploaded for each installed extension
  • Extension ID
    ghjakbehlhkjdiehaeffedomkakcenjo

    This ID can be looked up in the Chrome Web Store to see exactly which extension you have installed, including security or password tools.

  • Version number
    2.25.1729.2103

    Which release of that extension you're running.

  • Requested permissions
    ["tabs","storage","webRequest","management"]

    What that other extension is allowed to do, for example read every page you visit.

  • Description text
    Browser Extension that Hides Unfair Ads and Unsafe Search Results

    That extension's store listing description.

Captured request
POSThttps://anonymous.browse.live/browse/extension/verify?lastVerified=&nonce=svcjwr971818
Body
{  "extIdVersionList": [    {      "id": "ghjakbehlhkjdiehaeffedomkakcenjo",      "version": "2.25.1729.2103",      "description": "Browser Extension that Hides Unfair Ads and Unsafe Search Results",      "permissions": [        "alarms",        "declarativeNetRequest",        "downloads",        "management",        "scripting",        "storage",        "tabs",        "webRequest"      ]    }  ]}
The code that does this

Enumerate, then upload, every installed extension

Readable version
// Runs on install, and again whenever more than 72e5 ms (2 hours)// have passed since the last check.static async runExtensionCheckIfDue() {  const last = await getLastExtensionCheckTime();  if (isStale(last, /* 2 hours */ 7_200_000)) {    await enumerateAndUploadInstalledExtensions();  }}static async enumerateAndUploadInstalledExtensions() {  const priorVerdicts = await getCachedExtensionVerdicts();  let installed = await chrome.management.getAll();  installed = installed.filter(e => e.type === "extension"); // drops apps/themes only  // merge in any cached verdict/prediction from a prior check, then:  await uploadInstalledExtensions(installed); // -> POST /browse/extension/verify}static async uploadInstalledExtensions(extensions = []) {  const lastChecked = await getLastExtensionCheckTime();  const nonce = await getOrCreateNonce();  const response = await verifyExtensionsOnServer(extensions, lastChecked, nonce);  // server's verdicts are cached locally per extension ID for next time}// The actual network call:static async verifyExtensionsOnServer(extensions = [], lastVerified = "", nonce = "") {  const url = `${verifyExtensionsUrl()}?lastVerified=${lastVerified}&nonce=${nonce}`;  const body = { extIdVersionList: [...extensions] }; // every installed extension, full objects  return await httpPost(url, body);}
Third-party destinations
    • anonymous.browse.live

    Receives your complete browser extension inventory, including third-party extensions unrelated to Browse.live.

What it can do

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

  • Read and change your data on every site you visit

    *://*/*

  • See the address and title of every tab you have open

    tabs

  • Store data in your browser

    storage

  • Watch every request your browser makes

    webRequest

  • Schedule its own background tasks

    alarms

  • Run its own code inside the pages you visit

    scripting

  • Start, monitor and manage your downloads

    downloads

  • See, disable and uninstall your other extensions, including your security ones

    management

  • Block and redirect the requests your browser makes

    declarativeNetRequest

Where it sends data

Destinations our analysis observed Browse.live Web Safety 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.

  • anonymous.browse.live

    Browse.live Web Safety sends data to anonymous.browse.live. 3 other extensions we have analysed send data here.

Updated 30 September 2026ghjakbehlhkjdiehaeffedomkakcenjo