Is Hola VPN - Your Website Unblocker safe?

High risk

Hola VPN captures Chrome-internal identity headers from your Google requests and sends them to its servers every 10 minutes.

When the A/B test activates (affecting approximately 51% of Chrome desktop users), the extension registers a listener on all Google requests and reads privileged headers including X-Client-Data — a unique per-install Chrome fingerprint — along with browser channel, validation token, and user-agent. These are batched and uploaded via POST to client.hola.org every 10 minutes. No user action beyond installing the extension is required for collection to begin.

Hola VPN LTD.v1.257.990Chrome Web Store
75Risk

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

Hola VPN Harvests Chrome Identity Headers from Google Traffic

Every ten minutes, Hola VPN collects private Chrome headers Google attaches to your requests, including X-Client-Data (a unique install fingerprint), uploaded to Hola's servers.

Gated by an A/B test; starts after install with no prompt.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You install Hola VPN and visit any Google page.

No action beyond install is needed, collection starts automatically once the A/B test activates.

The extension did this

Hola captures private Chrome identity headers from your Google requests and uploads them to its servers every 10 minutes.

X-Client-Data is a unique per-install Chrome fingerprint; Hola collects it without a consent prompt, alongside your browser channel and validation token.

02EvidenceFIELD TABLE
Headers captured from your Google requests
FieldValueWhy it matters
Chrome Install ID
CJOHywE=A unique identifier tied to your Chrome install. Google uses it for experiments; Hola collecting it fingerprints your browser.
Browser Channel
stableIdentifies whether you are on Chrome Stable, Beta, Dev, or Canary.
Browser Validation Token
T/cMGWcFBicTCYvDwxRmMQ==A cryptographic token Chrome sends to Google to prove the request came from a legitimate Chrome build.
User-Agent
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36Your full browser version string including OS and Chrome version.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://client.hola.org/client_cgi/chrome_headers?be_ver=1.251.527
29 POST requests observed in dynamic analysis session after A/B flag forced on (rand=0.75); 17 of 29 included X-Client-Data.
Headers
Content-Typeapplication/json
Body
{
  "X-Client-Data": "CJOHywE=",
  "X-Browser-Channel": "stable",
  "X-Browser-Validation": "T/cMGWcFBicTCYvDwxRmMQ==",
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
}
04EvidenceCODE COMPARE
The code that does this

Chrome_headers class: header collection and upload logic

What it actually does
// Chrome_headers: captures Chrome identity headers from Google requests without a consent prompt
class Chrome_headers {
  constructor(conf) {
    this.conf = conf;
    this.data = {};   // accumulates captured headers between flushes
    this.listener = this._listener.bind(this);
  }

  // Called by A/B gate when test_send_chrome_headers value = 'on'
  activate() {
    chrome.webRequest.onSendHeaders.addListener(
      this.listener,
      { urls: ['*://*.google.com/*'] },
      // 'extraHeaders' is required to access privileged Chrome-internal headers
      // (X-Client-Data is not accessible to extensions without this flag)
      ['requestHeaders', 'extraHeaders']
    );
  }

  deactivate() {
    try { chrome.webRequest.onSendHeaders.removeListener(this.listener); }
    catch (e) { /* ignore */ }
  }

  // Generator: POSTs accumulated headers to Hola backend every ~10 min
  *_send() {
    yield be_bg_ajax.ccgi_ajax({
      url: conf.url_ccgi + '/chrome_headers?be_ver=' + version(),
      method: 'POST',
      data: this.data  // { 'X-Client-Data': 'CJOHywE=', 'User-Agent': '...', ... }
    });
  }
}
05EvidenceTEMPORAL PATTERN
When this fires
Every 10 minutes

Captured headers are flushed to Hola servers on a recurring 10-minute timer. Each flush uploads the accumulated header data from your Google browsing since the last send.

06EvidenceTHIRD PARTY LIST
Where your Chrome identity headers are sent
  • client.hola.org

    Hola Networks' primary API server. Receives harvested Chrome identity headers via POST /client_cgi/chrome_headers. Historically tied to the Bright Data (Luminati) proxy network.

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

Google x-client-data install ID and headers forwarded to a Hola server

Browsing *.google.com, Hola VPN reads Chrome-internal headers from outgoing requests, x-client-data, four x-browser-* headers, your User-Agent, then POSTs them to client.hola.org/client_cgi/chrome_headers.

New in 1.253.755.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You load any page on a *.google.com domain.

For example google.com, mail.google.com, or drive.google.com in a normal browsing tab.

The extension did this

The extension reads Chrome's internal request headers off that request and sends them to a Hola server.

It captures x-client-data, x-browser-channel, x-browser-copyright, x-browser-validation, x-browser-year and User-Agent, then POSTs them to client.hola.org/client_cgi/chrome_headers.

02EvidenceFIELD TABLE
What is read from each outgoing Google request and forwarded
FieldValueWhy it matters
Chrome install ID (x-client-data)
X-Client-Data: CJOHywE=A short token Chrome attaches only to Google requests. Forwarding it to a third party lets that party tie your separate visits together.
Full browser User-Agent
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36Your exact browser version and platform, with the full Chrome build number spliced in from a Client Hint.
Chrome build/validation headers
X-Browser-Channel: stable; X-Browser-Year: 2025Four Google-internal headers describing your Chrome channel, copyright year, build year and a validation token.
Google location-cookie shape (UULE)
uule_type: "w+CAIQICI", uule_length: 88The first 10 characters and total length of your Google UULE cookie, which encodes a location signal, are appended to the payload.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://client.hola.org/client_cgi/chrome_headers?be_ver=1.253.755
Captured during dynamic analysis: 15 POST requests from the background service worker to this endpoint after the A/B flag was set on and google.com was visited. The bodies carried the X-Client-Data token and full Chrome User-Agent string.
Headers
Content-Typeapplication/json
Body
{
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
  "X-Client-Data": "CJOHywE="
}
04EvidenceCODE COMPARE
The code that does this

The header-collection class in the background service worker

What it actually does
const chrome_header_names = ["x-browser-channel", "x-browser-copyright", "x-browser-validation", "x-browser-year", "x-client-data", "user-agent"];
const google_url_pattern = "*://*.google.com/*";

// _add_listener(): capture matching headers off every outgoing Google request
this.listener = details => {
  if (!details.requestHeaders) return;
  const headers = {};
  for (const hdr of details.requestHeaders) {
    if (chrome_header_names.includes(hdr.name.toLowerCase())) {
      headers[hdr.name] = hdr.value;
    }
  }
  if (!Object.keys(headers).length) return;
  if (ua_full_ver) {
    const ua_str = headers["User-Agent"] || headers["user-agent"] || "";
    if (ua_str) headers["User-Agent"] = ua_str.replace(/Chrome\/[\d.]+/, "Chrome/" + ua_full_ver);
  }
  this.data = headers;
  this.send();
};
self.chrome.webRequest.onSendHeaders.addListener(
  this.listener,
  { urls: [google_url_pattern] },
  ["requestHeaders", "extraHeaders"]
);

// send(): POST the captured headers to the Hola endpoint
yield E.be_bg_ajax.ccgi_ajax({
  url: conf.url_ccgi + "/chrome_headers?be_ver=" + version(),
  method: "POST",
  data: this.data,
  timeout: 10 * ms.SEC
});
05EvidenceCODE COMPARE
The code that does this

The server-side A/B flag that gates the feature

What it actually does
// default extension config: collection wiring is present and enabled...
collect_chrome_headers: { enabled: true, interval: 6e5 },  // 600,000 ms = 10 min

// ...but a server-controlled A/B test decides who actually runs it:
test_send_chrome_headers: {
  ver: 2,
  min_ver: "1.251.527",
  value: [[0, 1, "off"], [0, 0, "on"]],
  filter: { browser: "chrome" }
}

// gate at update_config():
if (ch_ab.test_send_chrome_headers != "on") ch_conf.enabled = false;
06EvidenceTHIRD PARTY LIST
Where the collected headers are sent
  • client.hola.org

    Receives the POSTed Chrome request headers at /client_cgi/chrome_headers. Operated by Hola Networks Ltd., the extension's publisher.

07EvidencePLAIN NOTE
Server-controlled gating

This collection is controlled by a server-side A/B flag (`test_send_chrome_headers`). In our first dynamic-analysis sessions on a fresh profile, the Hola server assigned the flag to `off` and no headers were collected. After we set the flag to `on` and visited google.com, the collection fired and we captured the POST requests shown above. Because the flag is decided server-side per install, whether any given user is currently in the collecting group is not visible from the extension UI.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

Bright Data Affiliate Domains Exempted From the VPN Tunnel

Hola VPN routes some domains DIRECT, outside the tunnel, exposing your real IP.

Version 1.251.527 added bvpn_domains, ten domains tied to Bright Data/Luminati products (earnapp.com, bright-sdk.com), routed like Hola's own infrastructure.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You turn on Hola VPN and visit one of ten Bright Data affiliate sites, such as earnapp.com or bright-sdk.com.

You expect every site to load through the VPN exit node while the VPN is on.

The extension did this

Hola routes that connection DIRECT instead of through the tunnel, so the site sees your real IP address rather than the VPN's.

These ten domains are treated identically to Hola's own infrastructure, which is also exempted from the tunnel.

02EvidenceFIELD TABLE
The ten domains added in v1.251.527 that bypass the VPN tunnel:
FieldValueWhy it matters
earnapp.com
earnapp.comBright Data's bandwidth-sharing / passive income product. Reached directly from your real IP.
bright-sdk.com
bright-sdk.comBright Data's SDK distribution domain. Connections to it leave your VPN tunnel.
brightvpn.com
brightvpn.comBrightVPN consumer product domain, owned by Bright Data / Luminati.
brightvideo.tv
brightvideo.tvBright Data affiliate media domain, exempted from the tunnel.
bright4good.eco
bright4good.ecoBright Data affiliate domain, exempted from the tunnel.
thepiggybox.net
thepiggybox.netBright Data affiliate domain, exempted from the tunnel.
freemovielibrary.tv
freemovielibrary.tvBright Data affiliate media domain, exempted from the tunnel.
rewardgum.com
rewardgum.comBright Data affiliate rewards domain, exempted from the tunnel.
boostnet.info
boostnet.infoBright Data affiliate domain, exempted from the tunnel.
screensavers.app
screensavers.appBright Data affiliate domain, exempted from the tunnel.
03EvidenceCODE COMPARE
The code that does this

How the ten domains get added to the always-DIRECT list, then drive the routing decision.

What it actually does
bvpn_domains appended to hola_domain_re
// js/bg.bg.bundle.js (zurl module), line ~54857
E.bvpn_domains = [
  "bright-sdk.com", "brightvpn.com", "earnapp.com", "brightvideo.tv",
  "bright4good.eco", "thepiggybox.net", "freemovielibrary.tv",
  "rewardgum.com", "boostnet.info", "screensavers.app"
];

function init_hola_domains_re() {
  // bvpn_domains are concatenated alongside Hola's own infrastructure domains.
  var domains = E.hola_domains
    .concat(additional_domains)
    .concat(E.hola_browser_internal_domains)
    .concat(E.hola_ext_domains)
    .concat(E.bvpn_domains);          // <-- the new Bright Data list
  return domains_re(domains);
}
var hola_domain_re = init_hola_domains_re();

E.is_hola_domain = function(domain) {
  return E.is_valid_domain(domain) && domain.search(hola_domain_re) != -1;
};
is_vpn_allowed returns DIRECT for hola domains
// js/bg.bg.bundle.js, is_vpn_allowed, line ~36527
if (is_all_browser_rule_active() || is_debug_peer_rule_active()) {
  return !bg_util.is_private_network(hostname, browser.isInNet)
      && !unblocker_lib.is_agent(hostname)
      && !/zagent\d+\.hola\.org/.test(hostname)
      && (is_debug_peer_rule_active() || !zurl.is_hola_domain(hostname));
  // is_hola_domain(hostname) === true  =>  returns false  =>  DIRECT (no VPN)
}
04EvidenceTHIRD PARTY LIST
Where these exempted connections go, and who owns the domains:
  • earnapp.com

    Bright Data / Luminati consumer bandwidth-sharing product. Reached from the user's real IP.

  • bright-sdk.com

    Bright Data SDK distribution domain.

  • brightvpn.com

    BrightVPN consumer product, Bright Data / Luminati.

05EvidenceARTIFACT
Reproduce it yourself

Reproduces the routing decision using the exact domain lists and regex from the extension's zurl module. Confirms that the ten bvpn_domains are classed as hola domains and get DIRECT (non-VPN) routing, while ordinary third-party sites route through the VPN.

RequiresNode.js 18+
hola-bvpn-pac-poc.js · js
// hola-bvpn-pac-poc.js
// Reproduces Hola VPN's PAC-style routing decision for bvpn_domains.
// Source: bg.bg.bundle.js zurl module (v1.253.755).

const hola_domains = ["hola.org","zspeed-cdn.com","h-vpn.org","holavpn.com","holavpnworld.com",
  "holavpnextension.com","holavpninstaller.com","holasof.com","holabrowser.com","holafreevpn.com",
  "holavpnrussia.com","hola-vpn.com","holax.io","holavpn.net","holavpnandroid.com"];

// NEW in v1.251.527 — absent from v1.249.511
const bvpn_domains = ["bright-sdk.com","brightvpn.com","earnapp.com","brightvideo.tv",
  "bright4good.eco","thepiggybox.net","freemovielibrary.tv","rewardgum.com",
  "boostnet.info","screensavers.app"];

const additional_domains = ["haffnetwork\\w{2}.com"];
const hola_ext_domains = ["shoopit.com","toolip.io","oculusproxies.com","browser.ai"];
const hola_browser_internal_domains = ["new-tab-page","hola-new-tab-page","hola-diagnostics","hola-settings","settings"];

function domains_re(arr) {
  const domains = arr.join("|").replace(/\./g, "\\.");
  return new RegExp("^(.*\\.)?(" + domains + ")$");
}
function init_hola_domains_re() {
  return domains_re(hola_domains
    .concat(additional_domains)
    .concat(hola_browser_internal_domains)
    .concat(hola_ext_domains)
    .concat(bvpn_domains));
}
const hola_domain_re = init_hola_domains_re();
function is_hola_domain(domain) {
  return /^([a-z0-9]([a-z0-9-_]*[a-z0-9])?\.)+[a-z]{2,63}$/.test(domain)
    && domain.search(hola_domain_re) !== -1;
}
// When all-browser VPN mode is active, is_vpn_allowed returns !is_hola_domain(host).
function vpn_allowed(host) { return !is_hola_domain(host); }

console.log("Bright Data domains (expected: DIRECT, real IP exposed):");
for (const d of bvpn_domains)
  console.log(`  ${d}: vpn_allowed=${vpn_allowed(d)} (${vpn_allowed(d) ? "VPN" : "DIRECT - real IP"})`);

console.log("\nControl third-party sites (expected: VPN):");
for (const d of ["google.com","amazon.com","example.com"])
  console.log(`  ${d}: vpn_allowed=${vpn_allowed(d)} (${vpn_allowed(d) ? "VPN" : "DIRECT"})`);
How to run it
  1. 1
    Save as hola-bvpn-pac-poc.js.
  2. 2
    Run `node hola-bvpn-pac-poc.js`.
  3. 3
    All ten bvpn_domains print vpn_allowed=false (DIRECT - real IP); control hosts print vpn_allowed=true (VPN).

Data recipients

client.hola.org
Updated 10 September 2026gkojfkhlekighikafcpjkiklfbnlmeio