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, 1 shared hostname
Reason Labs inc. - 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
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.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
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.
- Visited URLshttps://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 timestamp1712963847231
When each URL was visited, letting the server reconstruct your browsing timeline.
- Batch size99,847 URLs
Up to 100,000 URLs sent in a single request, your entire reachable browsing history.
200 OK, JSON object containing scan results per URL
- 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
Annotated — newScan(urls, options)
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 }}Annotated — navigation trigger (fires on every page load)
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() }]); }});Annotated — checkIfUrlIsSafe() allowlist check
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.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-829
- Source
- Dynamic sandbox
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.
- Browser localeen-US
The language and region setting of your browser.
- Extension version7.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 time1713400000000
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.
200 OK, SSE stream (retry:10000) delivering GrowthBook feature flag payload. Extension stores result in chrome.storage.local as gbFeaturesPayload and records fetch time in gbLastLoadedAt. Payload is reused for up to 24 hours (86,400,000 ms) before a fresh fetch.
- Accept
- text/event-stream
Config fetch and GrowthBook initialization from background.bundle.js:
GrowthBook client initialization (readable)
// 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() }); } }}Config POST to config.reasonsecurity.com/public (readable)
// 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), });}- traffic_source_protectiontrue
When on, the extension tracks referrers/navigations via webNavigation to detect traffic manipulation; off disables this for your session.
- block_page_detectionfalse
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_detectiontrue
Enables or disables referrer tracking across tab navigations.
- data_monitoring_notificationstrue
Controls whether you receive scheduled scan result notifications.
- facebook/tiktok id patternstrue
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.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
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).
- 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.
Enumerate, POST, disable (readable)
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; } }}The POST to the scan API (readable)
// Ir / postScanasync 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.
- Chrome with Developer mode enabled
// 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 analyzed sends data here.