Is Online Security safe?
Online Security reads up to 100,000 URLs from your browser history and sends them to a third-party server on every tab navigation.
On each tab switch, the extension queries the full browser history — up to 100,000 entries — and transmits the URL list as a JSON array to apis.reasonsecurity.com. A built-in allowlist of roughly 2,200 popular domains is skipped, but all other visited URLs are included in the upload. This behavior was confirmed through dynamic analysis.
Who publishes itReason Labs inc. - no other listings under this identity
Reason Labs inc. - 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
Your Full Browsing History Sent to ReasonSecurity on Every Page Load
Each time you visit a site, Online Security reads up to 100,000 URLs from your history and sends them in plain text to ReasonSecurity's servers, no consent prompt.
It skips 2,200 popular domains; every other site uploads within seconds.
You navigate to any website not on the extension's built-in allowlist.
The allowlist covers roughly 2,200 popular domains (Google, Facebook, YouTube, etc.), leaving the vast majority of sites unprotected.
The extension immediately reads up to 100,000 URLs from your browser history and sends them to ReasonSecurity's servers.
The POST fires within seconds of the page loading, before you have taken any deliberate action.
| Field | Value | Why it matters | |
|---|---|---|---|
Visited URLs | https://myhealth.example.com/results/blood-test-2024 | Every page you've opened in Chrome that isn't on the allowlist, including banking sites, health searches, and private browsing destinations. | |
Visit timestamp | 1712963847231 | When each URL was visited, letting the server reconstruct your browsing timeline. | |
Batch size | 99,847 URLs | Up to 100,000 URLs sent in a single request, your entire reachable browsing history. |
| Content-Type | application/json |
[ "https://news.ycombinator.com", "https://reddit.com/r/programming", "https://stackoverflow.com/questions/12345678", "https://mail.proton.me/u/0/inbox", "https://mybank.example.com/accounts" ]
The newScan() function, reads history and POSTs to ReasonSecurity
async newScan(urls, options) {
// Load cached safe/malicious URL lists from extension storage
const { safeLinks = {}, maliciousLinks = {} } = await storage.get();
const urlStrings = urls.map(({ url }) => url);
// Build the set of already-known-malicious links to skip
let knownMalicious = {};
if (options?.last30Days) {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - 30);
Object.keys(maliciousLinks).forEach(key => {
if (cutoff.getTime() < maliciousLinks[key].visitedAt)
knownMalicious[key] = maliciousLinks[key];
});
} else {
knownMalicious = { ...maliciousLinks };
}
// Filter: remove allowlisted and already-cached URLs
const { urlsToScan, httpProtocolUrls } =
await this.filterOutUrlsForScanning(urlStrings, knownMalicious, safeLinks);
if (urlsToScan.length > 0) {
// POST plain-text URL array to ReasonSecurity — no hashing, no encryption
const endpoint =
"https://apis.reasonsecurity.com/SSE/v1/scan/urls.ashx" +
(await featureFlag("block_page_detection") ? "?v=2" : "");
const response = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(urlsToScan), // raw URLs, no anonymisation
});
// ... process response
}
}webNavigation.onDOMContentLoaded.addListener(async ({ tabId, url, frameId }) => {
await this.init();
if (frameId !== 0) return; // top-level frame only
const hostname = new URL(url).hostname;
if (/* not a listed domain */) return;
// Skip allowlisted domains (Wa = ~2,200 popular sites)
if (this.checkIfUrlIsSafe(url)) return;
const settings = await storage.get();
if (settings.realTimeScan?.sites) {
// Trigger full history upload for every non-allowlisted page load
await this.newScan([{ url: normalise(url), visitedAt: Date.now() }]);
}
});checkIfUrlIsSafe(url) {
const rootDomain = getRootDomain(url); // Sr()
const normalisedUrl = normalise(url); // xr()
const { protocol } = new URL(url);
// Pass if domain is in the Wa hardcoded allowlist (~2,200 entries),
// or is localhost, or uses chrome:// scheme, or matches a built-in safe list
return !!allowlist[rootDomain] // Wa — e.g. google.com, facebook.com
|| rootDomain === "localhost"
|| protocol.includes("chrome")
|| Object.values(internalSafeList).includes(normalisedUrl);
// NOTE: everything else — banking, health, private pages — is NOT safe
}- apis.reasonsecurity.com
Primary scan API operated by ReasonSecurity (the developer of Online Security). Receives plain-text URL arrays from every user's browser history on each page navigation.
Remote Config Fetch Controls Scanning Behavior via Hardcoded API Key
Online Security contacts two servers on load. config.reasonsecurity.com, key-authed, returns a config for site auto-blocking. ab.reasonlabsapi.com serves a GrowthBook payload toggling six flags.
Confirmed: 200 SSE stream, keys in source.
You install or open Chrome with Online Security active.
The extension posts your locale, version, user ID, affiliate tag, and install time to config.reasonsecurity.com, then fetches feature flags from ab.reasonlabsapi.com.
Both requests use hardcoded API keys visible in the extension's source. The responses determine which protective features are active for your session.
| Field | Value | Why it matters | |
|---|---|---|---|
Browser locale | en-US | The language and region setting of your browser. | |
Extension version | 7.4.5 | Which version of Online Security is installed. | |
User ID (uuid / ruserid) | a1b2c3d4-e5f6-7890-abcd-ef1234567890 | A persistent identifier stored in chrome.storage.local that ties all requests back to your install. | |
Affiliate tag (aflt) | cws_organic | Records which distribution channel or partner led to this install. | |
Installation time | 1713400000000 | Unix timestamp of when the extension was first installed. | |
API key (x-api-key header) | cCU2RA0F | A hardcoded credential that authenticates the extension to the config server. The same key is used by all 13 million installs. |
| Accept | text/event-stream |
Config fetch and GrowthBook initialization from background.bundle.js:
// GrowthBook A/B testing client — initialized once at startup.
// Hardcoded clientKey identifies this extension to the server.
const growthbook = new GrowthBook({
apiHost: 'https://ab.reasonlabsapi.com',
clientKey: 'sdk-QtSYWOMLlkHBbNMB', // hardcoded, same for all installs
enableDevMode: false,
backgroundSync: false,
onFeatureUsage: (flagName, result) => {
usedFlags[flagName] = result.value;
Mixpanel.register({ features: usedFlags }); // flag values reported to analytics
},
});
async function initGrowthBook() {
const storage = await chrome.storage.local.get(['gbLastLoadedAt', 'gbFeaturesPayload']);
const lastLoaded = storage?.gbLastLoadedAt || 0;
const cached = storage?.gbFeaturesPayload || null;
const stale = (Date.now() - lastLoaded) >= 86_400_000; // 24-hour TTL
if (cached && !stale) {
await growthbook.init({ streaming: false, payload: cached }); // use cache
} else {
await growthbook.init({ streaming: false }); // fetch fresh from ab.reasonlabsapi.com
const payload = growthbook.getPayload();
if (payload) {
await chrome.storage.local.set({ gbFeaturesPayload: payload, gbLastLoadedAt: Date.now() });
}
}
}// Runs on startup and whenever configData.update() is called.
// The hardcoded API key authenticates to the config server.
async function fetchRemoteConfig(isFirstRun, trackingParams) {
const CONFIG_URL = 'https://config.reasonsecurity.com/public';
const API_KEY = 'cCU2RA0F'; // hardcoded, same for all 13M installs
const storage = await chrome.storage.local.get();
const { aflt, ruserid, random_number, installation_time } = storage?.eventParams || {};
const body = {
metaData: {
Locale: navigator.language,
product: 'online_security',
currentExtensionVersion: chrome.runtime.getManifest().version,
currentClientVersion: trackingParams?.productversion || '',
uuid: ruserid,
randomNumberForABTesting: random_number,
aflt: aflt,
freshInstallation: isFirstRun,
installation_time: installation_time,
}
};
return fetch(CONFIG_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': API_KEY },
body: JSON.stringify(body),
});
}| Field | Value | Why it matters | |
|---|---|---|---|
traffic_source_protection | true | When on, the extension tracks referrers/navigations via webNavigation to detect traffic manipulation; off disables this for your session. | |
block_page_detection | false | Switches the URL-scanning endpoint between /scan/urls.ashx and ?v=2. The server-picked version decides scan logic for every URL you visit. | |
tabs_referrers_detection | true | Enables or disables referrer tracking across tab navigations. | |
data_monitoring_notifications | true | Controls whether you receive scheduled scan result notifications. | |
facebook/tiktok id patterns | true | Pattern sets used when scanning for social-media ad identifiers in URLs. Server selects which patterns are active. |
- config.reasonsecurity.com
Primary config endpoint. Receives a startup POST with locale, version, user ID, affiliate tag; returns sites_auto_blocking and other settings. Operated by ReasonLabs.
- ab.reasonlabsapi.com
GrowthBook A/B platform. Returns a feature-flag SSE payload governing which scanning behaviors run, cached locally 24 hours. Operated by ReasonLabs.
Installed-extension IDs sent to a remote API that can auto-disable them
Online Security enumerates every other installed extension, POSTs the ID list to extgw.mozoapi.com/protection/extension/scan, and disables any marked non-"ok" via chrome.management.setEnabled.
A remote endpoint decides which stay enabled.
A scheduled scan runs (on startup, on idle, or on a recurring timer), no action from you is required.
The extension lists every other extension you have installed and sends all of their IDs to a remote scanning server.
For any extension the server marks as not "ok", the extension turns it off using chrome.management.setEnabled(id, false).
| Field | Value | Why it matters | |
|---|---|---|---|
ID of every installed extension | ["cjpalhdlnbpafiamejdnhcphjbkeiagm","gighmmpiobklfepjocnamgkkbiglidom","nkbihfbeghpgoedpieklcmphcijmoiob"] | A complete inventory of your other installed extensions, by Chrome Web Store ID. The extension's own ID is excluded. | |
Per-extension verdict | {"gighmmpiobklfepjocnamgkkbiglidom":{"status":"malicious"}} | The server's response status for each ID. Anything other than "ok" causes that extension to be disabled on your machine. |
The code that enumerates, transmits, and disables, from the shipped 7.5.0 source.
async scanAll() {
const all = await chrome.management.getAll();
const ids = [];
const byId = new Map();
all.forEach((ext) => {
if (ext.id === chrome.runtime.id || ext.type !== 'extension') return;
ids.push(ext.id); // collect every other extension's ID
byId.set(ext.id, { id: ext.id, name: ext.name, permissions: ext.permissions, ... });
});
let { extensions = {} } = await chrome.storage.local.get('extensions');
const verdicts = await postScan(ids); // POST the full ID list to the server
if (!verdicts) return extensions;
for (const [id, v] of Object.entries(verdicts)) {
const malicious = v.status !== 'ok'; // server decides
extensions[id] = { ...byId.get(id), malicious };
if (malicious && !extensions[id].whitelisted) {
await chrome.management.setEnabled(id, false); // disable it
extensions[id].enabled = false;
}
}
}// Ir / postScan
async function postScan(ids) {
return await httpFetch('https://extgw.mozoapi.com/protection/extension/scan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(ids), // raw array of extension IDs
});
}- extgw.mozoapi.com
Receives the full list of installed-extension IDs and returns per-ID verdicts. Operated by ReasonLabs/Mozo. Earlier versions used api.reasonsecurity.com.
Paste into the Online Security service-worker DevTools console. It wraps chrome.management.getAll, chrome.management.setEnabled, and fetch so you can watch the extension inventory being collected, the POST to the scan endpoint, and any setEnabled(false) calls that disable other extensions.
// online-security-scan-inspector.js
// Run in the Online Security service-worker DevTools console.
(function () {
const mgmt = chrome.management;
const origGetAll = mgmt.getAll.bind(mgmt);
mgmt.getAll = async function (...a) {
const r = await origGetAll(...a);
console.log('[SCAN] chrome.management.getAll ->', r.map(e => e.id));
return r;
};
const origSetEnabled = mgmt.setEnabled.bind(mgmt);
mgmt.setEnabled = function (id, enabled, ...rest) {
console.warn('[SCAN] setEnabled', id, enabled);
return origSetEnabled(id, enabled, ...rest);
};
const origFetch = self.fetch.bind(self);
self.fetch = function (url, opts) {
if (String(url).includes('/protection/extension/scan')) {
console.log('[SCAN] POST', url, opts && opts.body);
}
return origFetch(url, opts);
};
console.log('[SCAN_INSPECTOR] installed — trigger a scan to see activity.');
})();
- 1Open chrome://extensions, enable Developer mode.
- 2Click 'service worker' under Online Security.
- 3Paste this script in.
- 4Trigger a scan (or wait) and watch the [SCAN] log lines.
+1 more finding not shown
Where it sends data
Destinations our analysis observed Online Security 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.
- apis.reasonsecurity.com
Online Security sends data to apis.reasonsecurity.com. No other extension we have analysed sends data here.