Is BrowseHero: Maps & Travel Explorer safe?
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.
Who publishes itBrowseHero.me - no other listings under this identity
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.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
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.
You finish loading any page over HTTP or HTTPS, on any site.
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.
| Field | Value | Why it matters | |
|---|---|---|---|
Page URL you visited | https://www.examplebank.com/accounts/statements?acct=88213 | The full address of the page, only lightly redacted; the host and path are not stripped. | |
Page title | Statements - Example Bank | The title shown in the browser tab for that page. | |
Persistent client ID | 8f3e2a91-6c4d-4b7a-9e21-1a5c7d3f9b02 | A device ID created once at install and reused every time, letting requests be linked to the same person. | |
Extension install ID | gciolahndokdgchafbhbohmaaimdghbf | Chrome'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. |
Building and sending the per-page recommendation request
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 }
}
};
}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; }
}- api.browsehero.me
Receives the URL, title, persistent client ID, and tab metadata for every completed navigation, and returns the recommendation the extension displays.
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.
// 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('---');
}
- 1Save as redaction-check.js.
- 2Run `node redaction-check.js`.
- 3Compare each IN/OUT pair: the host and path are identical, only a few named query values change.
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.