Is BrowseHero: Maps & Travel Explorer safe?

Medium risk

BrowseHero sends the URL and title of nearly every page you visit to its own server and can overlay ads or open tabs based on the response.

On every page load across all sites, BrowseHero collects the page's URL, title, and browser tab details along with a persistent per-install id, then sends this to api.browsehero.me/feed to fetch a 'recommendation.' The extension's built-in redaction only strips emails and a few known query parameters, so the full page address and title are otherwise preserved. Depending on the server's response, BrowseHero can open a new tab, group tabs, or display a full-page overlay ad on top of the page you're viewing.

BrowseHero.mev1.0.4Chrome Web Store
45Risk
Who publishes it

BrowseHero.me - no other listings under this identity

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

Publisher
BrowseHero.me

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

BrowseHero sends every page URL and title you visit to api.browsehero.me/feed

Code analysis shows BrowseHero's background service worker sends the URL, title, and a persistent client ID for nearly every page you load to api.browsehero.me/feed.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You finish loading any page over HTTP or HTTPS, on any site.

The extension did this

The background service worker packages that page's URL and title with a persistent client ID and posts it to api.browsehero.me/feed to fetch a recommendation.

The response can open a new tab, group the current tab, or show a full-page overlay ad.

02EvidenceFIELD TABLE
What each navigation sends to api.browsehero.me/feed
FieldValueWhy it matters
Page URL you visited
https://www.examplebank.com/accounts/statements?acct=88213The full address of the page, only lightly redacted; the host and path are not stripped.
Page title
Statements - Example BankThe title shown in the browser tab for that page.
Persistent client ID
8f3e2a91-6c4d-4b7a-9e21-1a5c7d3f9b02A device ID created once at install and reused every time, letting requests be linked to the same person.
Extension install ID
gciolahndokdgchafbhbohmaaimdghbfChrome's unique identifier for this extension install.
Tab and window details
{"tabId":482,"windowId":901,"focused":true,"numberOfTabs":6}Tab ID, window ID, whether the tab is focused or fullscreen, and how many tabs are open nearby.
03EvidenceCODE COMPARE
The code that does this

Building and sending the per-page recommendation request

What it actually does
generateRequestPayload(), annotated
async function generateRequestPayload(tab, tabDetails) {
  // Pull local prefs, remote config, and the ticket assigned by the server.
  const { snoozed, preferGroup, preferNewTab, preferOverlay } = await preferences_controller.getPreferences();
  const config = await config_controller.get();
  const ticket = await ticket_controller.get();
  const win = await browser.windows.get(tab.windowId, { populate: true });

  // If this tab belongs to a tab group, count the group's tabs too.
  let group = {};
  const groupDetails = await group_controller.getById(tab.groupId);
  if (groupDetails) {
    const groupTabs = await browser.tabs.query({ groupId: groupDetails?.id });
    const details = await tabs_controller.getTabDetails(tab.id);
    if (details) {
      group = { id: groupDetails.id, numberOfTabs: groupTabs.length, sponsored: !!details.sponsored };
    }
  }

  return {
    client: { id: config.client.id },              // persistent per-install ID
    extension: { guid: chrome.runtime.id, version: chrome.runtime.getManifest().version, ticket },
    preferences: { snoozed, group: preferGroup, newTab: preferNewTab, overlay: preferOverlay },
    page: {
      url: deidentifyUser(tab.url),                // lightly redacted full URL
      title: deidentifyUser(tab.title),             // lightly redacted page title
      group,
      tab: { id: tab.id, parentId: tabDetails.openerTabId, containerId: tab.windowId, focused: tab.active, fullScreen: win.state === 'fullscreen' },
      container: { id: tab.windowId, type: win.type, numberOfTabs: win.tabs?.length },
      meta: { name: tabDetails.lpInfo?.id }
    }
  };
}
getRecommendations(), annotated
async function getRecommendations(tab, tabDetails) {
  try {
    let requestPayload = null;
    try {
      requestPayload = await generateRequestPayload(tab, tabDetails);
    } catch (ex) { console.log(ex); }

    // recommendationUrl is 'https://api.browsehero.me/feed' (settings.ts).
    const url = background_settings.get('recommendationUrl');
    const controller = new AbortController();
    requests[tab.id] = controller;

    // Cancel the in-flight request if the user navigates away or closes the tab first.
    const onTabUpdated = (tabId, changeInfo, updatedTab) => {
      if (updatedTab.id === tab.id && updatedTab.url !== tab.url && requests[tabId]) {
        requests[tabId].abort();
        delete requests[tabId];
      }
    };
    const onTabRemoved = (tabId) => {
      if (tabId === tab.id) { requests[tabId]?.abort(); delete requests[tabId]; }
    };

    const response = http.post(url, { body: JSON.stringify(requestPayload), signal: controller.signal }).catch(() => null);
    chrome.tabs.onUpdated.addListener(onTabUpdated);
    chrome.tabs.onRemoved.addListener(onTabRemoved);
    response.finally(() => {
      chrome.tabs.onUpdated.removeListener(onTabUpdated);
      chrome.tabs.onRemoved.removeListener(onTabRemoved);
    });
    return response;
  } catch { return null; }
}
04EvidenceTHIRD PARTY LIST
Third-party destinations
  • api.browsehero.me

    Receives the URL, title, persistent client ID, and tab metadata for every completed navigation, and returns the recommendation the extension displays.

05EvidenceARTIFACT
Reproduce it yourself

Runs BrowseHero's own redaction functions, copied verbatim from background.js, against sample URLs to show the host and path reach api.browsehero.me/feed unchanged.

RequiresNode.js 14 or newer (uses only the built-in URL global)
redaction-check.js · js
// redaction-check.js
// The deidentifyQueryParams / deidentifyGlobally / deidentifyUser functions
// below are copied verbatim from the extension's shipped background.js.
// Run this against sample URLs to see exactly what survives before the
// request reaches api.browsehero.me/feed.

function deidentifyQueryParams(url) {
  const queryRegex = [
    { name: 'TEL', paramRegex: /((tel)|(telephone)|(phone)|(mobile)|(mob))/gi, valueRegex: /[\d\+\s][^&\/\?]+/gi },
    { name: 'NAME', paramRegex: /((firstname)|(lastname)|(surname))/gi, valueRegex: /[^&\/\?]+/gi },
    { name: 'PASSWORD', paramRegex: /((password)|(passwd)|(pass))/gi, valueRegex: /[^&\/\?]+/gi },
    { name: 'ZIP', paramRegex: /((postcode)|(zipcode)|(zip))/gi, valueRegex: /[^&\/\?]+/gi },
    { name: 'TOKEN', paramRegex: /((access_token)|(token))/gi, valueRegex: /[^&\/\?]+/gi }
  ];
  const data = url.replace(/(^\?)/, '').split('&').map(function (n) {
    n = n.split('=');
    this[n[0]] = n[1];
    return this;
  }.bind({}))[0];
  for (const key in data) {
    queryRegex.forEach(pii => {
      const val = data[key];
      if (key.match(pii.paramRegex)) {
        data[key] = val.replace(pii.valueRegex, `[REDACTED_${pii.name}]`);
      }
    });
  }
  return Object.keys(data).map(key => key + (data[key] ? `=${data[key]}` : '')).join('&');
}

function deidentifyGlobally(url) {
  const globalRegex = [
    { name: 'EMAIL', regex: /[^\/]{4}(@|%40)[^\/]{4}/gi },
    { name: 'ANCHOR', regex: /#.*$/gi }
  ];
  globalRegex.forEach(pii => { url = url.replace(pii.regex, `[REDACTED_${pii.name}]`); });
  return url;
}

function isValidUrl(text) {
  try { new URL(text); return true; } catch (_) { return false; }
}

function deidentifyUser(data) {
  try {
    if (isValidUrl(data)) return deidentifyGlobally(deidentifyQueryParams(data));
    return deidentifyGlobally(data);
  } catch (e) { return data; }
}

const samples = [
  'https://www.examplebank.com/accounts/statements?acct=88213&ref=partner',
  'https://portal.examplehealth.com/patient/record?id=48213&visit=annual',
  'https://mail.example.com/inbox?user=jsmith%40example.com'
];

for (const url of samples) {
  console.log('IN: ', url);
  console.log('OUT:', deidentifyUser(url));
  console.log('---');
}
How to run it
  1. 1
    Save as redaction-check.js.
  2. 2
    Run `node redaction-check.js`.
  3. 3
    Compare each IN/OUT pair: the host and path are identical, only a few named query values change.
06EvidencePLAIN 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 1.0.4. 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 secure site you visit

    https://*/*

  • Read and change your data on every site you visit

    http://*/*

  • Store data in your browser

    storage

  • Schedule its own background tasks

    alarms

  • Read and change your tab groups

    tabGroups

Where it sends data

Destinations our analysis observed BrowseHero: Maps & Travel Explorer 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.

  • api.browsehero.me

    BrowseHero: Maps & Travel Explorer sends data to api.browsehero.me. One other extension we have analysed sends data here.

Updated 30 September 2026gciolahndokdgchafbhbohmaaimdghbf