Is Online Security safe?

High risk

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.

ReasonLabsv7.6.0Chrome Web Store
77Risk
Who publishes it

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.

Publisher
ReasonLabs
Declared legal entity
Reason Labs inc.
Registered address
228 Park Ave S, New York, NY 10003-1502, US
Registered contact
Reason Labs inc.

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.

webtorrent.io
Also called by 4 other listings, including Encyclosphere, Shake Torrent

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
What actually happens
You did this

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 did this

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.

What gets uploaded in every POST request
  • 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.

Captured request
POSThttps://apis.reasonsecurity.com/SSE/v1/scan/urls.ashx

200 OK, JSON object containing scan results per URL

Headers
Content-Type
application/json
Body
[  "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 code that does this

The newScan() function, reads history and POSTs to ReasonSecurity

Readable version

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}
Where your browsing history is sent
    • 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
What actually happens
You did this

You install or open Chrome with Online Security active.

The extension did this

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.

Fields sent in the POST to config.reasonsecurity.com/public
  • 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.

Captured request
GEThttps://ab.reasonlabsapi.com/sub/sdk-QtSYWOMLlkHBbNMB

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.

Headers
Accept
text/event-stream
The code that does this

Config fetch and GrowthBook initialization from background.bundle.js:

Readable version

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),  });}
GrowthBook feature flags that control extension behavior
  • 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.

Servers that control extension behavior
    • 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
What actually happens
You did this

A scheduled scan runs (on startup, on idle, or on a recurring timer), no action from you is required.

The extension did this

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).

What is sent to the scan server, and what comes back
  • 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 does this

The code that enumerates, transmits, and disables, from the shipped 7.5.0 source.

Readable version

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  });}
Where the extension inventory is sent
    • 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.

Reproduce it yourself

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.

Requires
  • Chrome with Developer mode enabled
online-security-scan-inspector.js · js
// 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.');})();
How to run it
  1. 1Open chrome://extensions, enable Developer mode.
  2. 2Click 'service worker' under Online Security.
  3. 3Paste this script in.
  4. 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.

Updated 30 September 2026llbcnfanfmjhpedaedhbcnpgeepdnnok