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
75Risk
Who publishes it

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.

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.

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
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.

02EvidenceFIELD TABLE
What gets uploaded in every POST request
FieldValueWhy it matters
Visited URLs
https://myhealth.example.com/results/blood-test-2024Every page you've opened in Chrome that isn't on the allowlist, including banking sites, health searches, and private browsing destinations.
Visit timestamp
1712963847231When each URL was visited, letting the server reconstruct your browsing timeline.
Batch size
99,847 URLsUp to 100,000 URLs sent in a single request, your entire reachable browsing history.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://apis.reasonsecurity.com/SSE/v1/scan/urls.ashx
200 OK, JSON object containing scan results per URL
Headers
Content-Typeapplication/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"
]
04EvidenceCODE COMPARE
The code that does this

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

What it actually does
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
}
05EvidenceTHIRD PARTY LIST
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.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-829
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
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.

02EvidenceFIELD TABLE
Fields sent in the POST to config.reasonsecurity.com/public:
FieldValueWhy it matters
Browser locale
en-USThe language and region setting of your browser.
Extension version
7.4.5Which version of Online Security is installed.
User ID (uuid / ruserid)
a1b2c3d4-e5f6-7890-abcd-ef1234567890A persistent identifier stored in chrome.storage.local that ties all requests back to your install.
Affiliate tag (aflt)
cws_organicRecords which distribution channel or partner led to this install.
Installation time
1713400000000Unix timestamp of when the extension was first installed.
API key (x-api-key header)
cCU2RA0FA hardcoded credential that authenticates the extension to the config server. The same key is used by all 13 million installs.
03EvidenceNETWORK CAPTURE
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
Accepttext/event-stream
04EvidenceCODE COMPARE
The code that does this

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

What it actually does
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),
  });
}
05EvidenceFIELD TABLE
GrowthBook feature flags that control extension behavior:
FieldValueWhy it matters
traffic_source_protection
trueWhen on, the extension tracks referrers/navigations via webNavigation to detect traffic manipulation; off disables this for your session.
block_page_detection
falseSwitches 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
trueEnables or disables referrer tracking across tab navigations.
data_monitoring_notifications
trueControls whether you receive scheduled scan result notifications.
facebook/tiktok id patterns
truePattern sets used when scanning for social-media ad identifiers in URLs. Server selects which patterns are active.
06EvidenceTHIRD PARTY LIST
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.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
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).

02EvidenceFIELD TABLE
What is sent to the scan server, and what comes back:
FieldValueWhy 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.
03EvidenceCODE COMPARE
The code that does this

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

What it actually does
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 / 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
  });
}
04EvidenceTHIRD PARTY LIST
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.

05EvidenceARTIFACT
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.

RequiresChrome 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. 1
    Open chrome://extensions, enable Developer mode.
  2. 2
    Click 'service worker' under Online Security.
  3. 3
    Paste this script in.
  4. 4
    Trigger 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.

Updated 30 September 2026llbcnfanfmjhpedaedhbcnpgeepdnnok