Is SetupVPN - Lifetime Free VPN safe?

High risk

SetupVPN encrypts all its API traffic with a per-request XOR key, making it impossible to inspect what data is being sent.

Code analysis found that all communication with the extension's backend is XOR-encrypted using a key included in each request header, rendering standard traffic inspection ineffective. The extension also contains a Go-compiled WebAssembly module with a hardcoded AES key. The content of the transmitted data is technically recoverable but has not yet been analyzed.

SetupVPN Incv4.0.9Chrome Web Store
80Risk
Who publishes it

SetupVPN Inc - 2 other listings from the same operator, none carrying a finding

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

Publisher
SetupVPN Inc
Declared legal entity
SetupVPN Inc
Registered address
815 Ponce de Leon Suite 210, Miami, FL 33134-3038, US
Registered contact
Osman Taskin

Same operator - 2 listings

Published under a different store account, but sharing the registered address, contact or declared legal entity this one gave the store.

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

Publishers can request a review.

Findings

Remote Config: 7-Mirror CDN Rotates All API Infrastructure Every 6 Hours

SetupVPN doesn't hardcode servers.

It fetches tierssv.json from one of 7 CDN mirrors every 6h, listing domains to use next, letting operators swap endpoints with no update.

Mirrors: DigitalOcean, S3, R2, Vultr, Linode, GitHub, Bitbucket.

Severity
High unwanted
Type
Unexpected
CWE
CWE-829
Source
Dynamic sandbox
What actually happens
You did this

You install SetupVPN or open Chrome after installing it.

The config fetch fires once at install and then every 6 hours via a Chrome alarm, no user interaction needed.

The extension did this

The extension downloads a server config file from one of 7 CDN mirrors and replaces all API endpoints.

The new mainbase and tierbase domain lists immediately take effect for all future requests. The operator can push completely different infrastructure to 9 million users within 6 hours.

Captured request
GEThttps://tierbase3.fra1.cdn.digitaloceanspaces.com/tierssv.json

200 OK, JSON, ~2 KB, contains mainbase[] (12 domains), tierbase[] (5 domains), update_interval_hours: 6

Headers
Origin
chrome-extension://oofgbpoabipfcfjapgnbbjjaenockbdp
Sec-Fetch-Mode
cors
Sec-Fetch-Site
cross-site
What's stored on your device

The live server config from CDN. mainbase lists the 12 domains the extension sends data to; the operator can push a new list any time.

Location
chrome.storage.local key 'ext_serversjson'
Contents
{  "retcode": 200,  "data": {    "version": 4,    "update_interval_hours": 6,    "settings": {      "tierbase_search_timeout": 15000,      "mainbase_search_timeout": 20000,      "tierbase_api_timeout": 35000,      "mainbase_api_timeout": 30000,      "proxy_search_timeout": 30000,      "mirror_timeout": 20000,      "base_ttl_days": 90,      "bs_consequtive_fails": 42    },    "mainbase": [      "https://lllm.scanners.fun",      "https://uabh.talked.run",      "https://mjgu.figure.run",      "https://xcxx.pointed.cc",      "https://uaia.scanners.fun",      "https://icax.figure.run",      "https://1.foreground.work",      "https://1.awakened.work",      "https://1.6912044.cc",      "https://1.default2024.uk",      "https://1.sahi.uk",      "https://1.area9.uk"    ],    "tierbase": [      "https://sxkk.pointed.cc",      "https://api.keepthisdomain.com",      "https://1.allnine.uk",      "https://ksho.uk",      "https://3245.uk"    ],    "uibase": [      "https://user1.setupvpn.com",      "https://user2.setupvpn.com",      "https://user3.setupvpn.com",      "https://user4.setupvpn.com",      "https://user5.setupvpn.com",      "https://user6.setupvpn.com",      "https://user7.setupvpn.com",      "https://user8.setupvpn.com",      "https://user9.setupvpn.com",      "https://user10.setupvpn.com",      "https://user11.setupvpn.com",      "https://user13.setupvpn.com",      "https://user14.setupvpn.com",      "https://user15.setupvpn.com"    ],    "mirrors": [      "https://tierbase3.fra1.cdn.digitaloceanspaces.com/tierssv.json",      "https://tierbase4.s3.amazonaws.com/tierssv.json",      "https://pub-8029ed10cf4e4db0b3757e6b82ef7a40.r2.dev/tierssv.json",      "https://ams1.vultrobjects.com/tierupdate2/tierssv.json",      "https://mirror4.es-mad-1.linodeobjects.com/tierssv.json",      "https://raw.githubusercontent.com/the7c/update/master/master/ui/data.json",      "https://bitbucket.org/the7c/update/raw/master/edge/pub/data.json"    ],    "t": 1736093012935  }}
The code that does this

Mirror fetch and server config dispatch (background.bundle.js)

Readable version

Fallback config and mirror URLs — readable

// UPDATE_SERVERSJSON is the Redux action type that replaces all API endpoints.const UPDATE_SERVERSJSON = 'UPDATE_SERVERSJSON';// Hardcoded fallback config — used only if no CDN mirror is reachable.// In practice, getTiers() fires at startup and CDN response overwrites this immediately.const DEFAULT_SERVER_CONFIG = { retcode: 200, data: { version: 4, update_interval_hours: 6, // server can change this mainbase: [ // 12 API endpoints — all fingerprinted requests go here 'https://lllm.scanners.fun', 'https://uabh.talked.run', 'https://mjgu.figure.run', 'https://xcxx.pointed.cc', 'https://uaia.scanners.fun', 'https://icax.figure.run', 'https://1.foreground.work', 'https://1.awakened.work', 'https://1.6912044.cc', 'https://1.default2024.uk', 'https://1.sahi.uk', 'https://1.area9.uk' ], tierbase: [ // 5 secondary endpoints 'https://sxkk.pointed.cc', 'https://api.keepthisdomain.com', 'https://1.allnine.uk', 'https://ksho.uk', 'https://3245.uk' ], mirrors: [ // 7 CDN mirrors tried in order 'https://tierbase3.fra1.cdn.digitaloceanspaces.com/tierssv.json', 'https://tierbase4.s3.amazonaws.com/tierssv.json', 'https://pub-8029ed10cf4e4db0b3757e6b82ef7a40.r2.dev/tierssv.json', 'https://ams1.vultrobjects.com/tierupdate2/tierssv.json', 'https://mirror4.es-mad-1.linodeobjects.com/tierssv.json', 'https://raw.githubusercontent.com/the7c/update/master/master/ui/data.json', 'https://bitbucket.org/the7c/update/raw/master/edge/pub/data.json' ] }};

getTiers interval setup — readable

// On startup: create a repeating alarm named 'int1' tied to getTiers().// updateIntervalTime is derived from update_interval_hours in the last fetched config.this.updateInterval = new IntervalTimer( this.getTiers.bind(this), this.updateIntervalTime, // default 6 hours = 21600000ms; server-adjustable 'int1' // alarm name visible in chrome.alarms);this.updateInterval.start();this.getTiers(); // also fires immediately at startup// isValidTierResponse() accepts the response only if:// retcode === 200// mainbase.length > 2// tierbase.length > 2// update_interval_hours must be integer 1 < x < 168 (1 week max)
CDN mirrors hosting the server config
    • tierbase3.fra1.cdn.digitaloceanspaces.com

    Primary CDN mirror (DigitalOcean Spaces, Frankfurt). Confirmed fetched in dynamic analysis session. Hosts tierssv.json, the extension infrastructure config.

    • tierbase4.s3.amazonaws.com

    AWS S3 mirror. Fallback if DigitalOcean mirror is unreachable. Bucket name: tierbase4.

    • pub-8029ed10cf4e4db0b3757e6b82ef7a40.r2.dev

    Cloudflare R2 object storage mirror. The hex ID is the R2 bucket public access subdomain.

    • ams1.vultrobjects.com

    Vultr Object Storage mirror (Amsterdam). Path: /tierupdate2/tierssv.json.

    • mirror4.es-mad-1.linodeobjects.com

    Linode Object Storage mirror (Madrid). Five-CDN redundancy makes the config highly available and resistant to domain blocking.

    • raw.githubusercontent.com

    GitHub raw file mirror. Repository: the7c/update, path master/master/ui/data.json. Public repo acts as a free, highly available CDN fallback.

    • bitbucket.org

    Bitbucket raw file mirror. Repository: the7c/update, path master/edge/pub/data.json. Same operator (user the7c) controls both GitHub and Bitbucket mirrors.

WASM Fingerprint: Device + Browser Profiling on Every API Call

SetupVPN embeds a Go WASM binary (534KB) fingerprinting the browser at startup, no prompt: screen size, storage quota, heap size, mouse movement, incognito state, DevTools/timezone/Selenium detection, on every POST, XOR-encrypted, claim135.

Severity
High unwanted
Type
Unexpected
CWE
CWE-359
Source
Dynamic sandbox
What actually happens
You did this

You open Chrome with SetupVPN installed.

No action is required; the fingerprint is collected automatically at extension startup.

The extension did this

The extension builds a device fingerprint and attaches it to every request to SetupVPN servers.

A Go-compiled WASM binary exports 9 fingerprinting symbols that are called by the background service worker. The results are merged into the params for all POST requests via po().

Fingerprint data collected by the WASM module
  • Screen dimensions
    outerWidth=1920, innerWidth=1904, outerHeight=1080, innerHeight=969

    Outer and inner width/height of your browser window, helps uniquely identify your display configuration.

  • Storage quota
    12884901888 bytes (12 GB)

    How much temporary storage your browser has allocated, a stable, hard-to-spoof device fingerprint signal.

  • JS heap size limit
    4294705152 bytes (4 GB)

    Maximum memory your browser process can allocate, varies by OS, Chrome version, and hardware.

  • Mouse movement data
    mouseMovement=[{x:413,y:208,t:104},{x:414,y:209,t:118}]

    The speed and pattern of your mouse movements, used as a biometric signal and bot-detection measure.

  • Incognito / private mode
    false

    Whether you are browsing in an incognito window, changes the risk profile of your session for the server.

  • DevTools open
    false

    Whether you have the browser developer tools open, extension uses this to detect researchers or power users.

  • Selenium / automation detection
    false

    Whether your browser is being controlled by automated test software, used to detect security researchers.

  • Timezone mismatch
    false

    Whether your system timezone differs from what the browser reports, a VPN or proxy detection signal.

  • OS and platform
    os=mac, platform=desktop, brand=Chrome

    Your operating system type and Chrome build variant, narrows down your device further.

The code that does this

WASM fingerprint proxy + request construction (background.bundle.js)

Readable version

WASM instantiation — readable

// Loads main.wasm and wraps all exported Go functions in a Promise-based proxy.const zi = (() => {  let wasmReady = false;  async function initWasm(wasmBytesPromise) {    const go = new Go();  // Go WASM runtime helper (wasm_exec.js)    const bytes = await wasmBytesPromise;    const instance = await WebAssembly.instantiate(bytes, go.importObject);    go.run(instance.instance);    wasmReady = true;  }  initWasm(fetch('/main.wasm').then(r => r.arrayBuffer()));  return new Proxy({}, {    get(_, methodName) {      return (...args) => new Promise((resolve, reject) => {        const poll = () => {          if (!wasmReady) { setTimeout(poll, 10); return; }          window[methodName](...args, (err, ...result) => {            if (err) reject(err); else resolve(result);          });        };        poll();      });    }  });})();

Request pipeline — readable

// Every POST request to SetupVPN servers goes through po().async function po(endpoint, params, { signal, baselink, requestUrl, timeout, defaults } = {}) {  const key = generateRandomKey();          // go(): 3-8 random alphanumeric chars  const headers = buildAuthHeader(key);     // mo(): {Content-Type, Authorization: Basic}  const requestOptions = { method: 'POST', headers };  const defaultParams = defaults ? defaults : await getOsDefaults(); // vo()  const mergedParams = { ...params, ...defaultParams };  requestOptions.body = encodeBody(mergedParams, key); // bo(): url-encode -> XOR -> base64  const controller = new AbortController();  requestOptions.signal = controller.signal;  const timer = setTimeout(() => controller.abort(), timeout);  const response = await fetch(requestUrl || (baselink + endpoint), requestOptions);  clearTimeout(timer);  return response;}// XOR cipher (Eo)function xorCipher(text, key) {  const t = text.split('');  const k = key.split('');  for (let i = 0; i < t.length; i++)    t[i] = String.fromCharCode(t[i].charCodeAt(0) ^ k[i % k.length].charCodeAt(0));  return t.join('');}function encodeBody(params, key) {  const urlEncoded = Object.keys(params)    .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))    .join('&');  return btoa(xorCipher(urlEncoded, key));}function decodeResponse(body, key) {  return xorCipher(decodeURIComponent(escape(atob(body))), key);}
SetupVPN mainbase domains receiving fingerprinted requests
    • lllm.scanners.fun

    Primary mainbase API endpoint. Receives all user telemetry, auth, and fingerprinted payloads. Domain registered via Namecheap; no obvious business identity.

    • uabh.talked.run

    Secondary mainbase endpoint. Same API as lllm.scanners.fun. Used as failover when primary is unreachable.

    • mjgu.figure.run

    Tertiary mainbase endpoint. Obfuscated subdomain on figure.run.

    • xcxx.pointed.cc

    Mainbase endpoint on pointed.cc infrastructure. Confirmed contacted in DA traffic.

    • uaia.scanners.fun

    Additional scanners.fun mainbase endpoint.

    • icax.figure.run

    Additional figure.run mainbase endpoint.

    • 1.foreground.work

    Mainbase endpoint on foreground.work.

    • 1.awakened.work

    Mainbase endpoint on awakened.work.

Dual-Layer Encryption Hides All Traffic: XOR + Go WASM AES

SetupVPN wraps every request in two layers.

The outer is a rolling XOR cipher: a random key scrambles the body and travels with it in the same header, reversible by anyone intercepting both. main.wasm adds a Go AES layer with a 44-char key.

Severity
High unwanted
Type
Unexpected
CWE
CWE-506
Source
Dynamic sandbox
What actually happens
You did this

The extension prepares any POST request to a SetupVPN server.

This applies to every API call, authentication, server selection, telemetry, and config fetches.

The extension did this

The request body is XOR-encrypted with a per-request random key, and the key is embedded in the Authorization header.

A Go-compiled WASM binary applies an additional AES cipher layer using a 44-character key hardcoded in the binary: 'JHn7tK7OjOWMHHPm2wEgeS3E8Gbq3PPgZx3dnj3Xx?q!!'

Why you can't catch this in DevTools

All POST bodies look like random base64 strings in network tools. Decoding requires extracting the XOR key from the Authorization header.

Decoded value
login=user%40example.com&hpassword=<redacted>&os=mac&cv=4.7.2.2&platform=desktop&brand=Chrome
The code that does this

The XOR cipher and encoding pipeline, shipped vs readable

Readable version

XOR cipher and encoding pipeline — readable

// Eo(text, key) — character-level XOR cipher with a repeating key.function xorCipher(text, key) { const textChars = text.split(''); const keyChars = key.split(''); for (let i = 0; i < textChars.length; i++) { textChars[i] = String.fromCharCode( textChars[i].charCodeAt(0) ^ keyChars[i % keyChars.length].charCodeAt(0) ); } return textChars.join('');}// go() — generates a random 3-65 char alphanumeric key (3-8 when used in mo())function generateKey(length) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; return Array.from({ length }, () => chars[Math.floor(Math.random() * chars.length)]).join('');}// mo(key) — builds request headers; the XOR key goes in Authorization: Basicfunction buildHeaders(key) { const suffix = generateKey(Math.round(3 + 5 * Math.random())); return { 'Content-Type': 'text/plain', 'Authorization': 'Basic ' + btoa(key + ':' + suffix) };}// bo(params, key) — encodes the request bodyfunction encodeBody(params, key) { const urlEncoded = Object.keys(params) .filter(k => Object.prototype.hasOwnProperty.call(params, k)) .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])) .join('&'); return btoa(xorCipher(urlEncoded, key));}// wo(body, key) — decodes a response bodyfunction decodeBody(encodedBody, key) { return xorCipher(decodeURIComponent(escape(atob(encodedBody))), key);}
Reproduce it yourself

Decodes any captured SetupVPN POST request body using the XOR key extracted from its Authorization header. Paste in the captured Authorization header value and POST body, run the script, and it prints the plaintext params sent by the extension.

Requires
  • Node.js 16+
setupvpn-xor-decode.js · js
#!/usr/bin/env node// setupvpn-xor-decode.js//// Decodes a captured SetupVPN POST request body.//// How SetupVPN encodes requests (from background.bundle.js functions Eo/bo/mo/go/wo):// 1. Generate a random key K (3-8 alphanumeric chars) — function go()// 2. URL-encode all request params into a query string P — inside bo()// 3. XOR every character of P with repeating K -> C — function Eo()// 4. base64-encode C -> wire body — btoa() in bo()// 5. Put K in the Authorization: Basic base64(K:randomSuffix) header — function mo()//// To decode: atob(body) -> unescape(escape(result)) -> XOR(result, K)// To get K: atob(authHeaderValue.replace("Basic ", "")).split(":")[0]//// Usage:// 1. Capture a POST to any mainbase domain (lllm.scanners.fun, uabh.talked.run, etc.)// 2. Copy the value from the Authorization header (everything after "Basic ")// 3. Copy the raw base64 POST body// 4. Paste them below as CAPTURED_AUTH_HEADER and CAPTURED_BODY// 5. node setupvpn-xor-decode.js//// Requirements: Node.js 16+ (no npm packages needed)// ─── PASTE YOUR CAPTURED VALUES HERE ────────────────────────────────────────// The value from the Authorization header (everything after "Basic "):const CAPTURED_AUTH_HEADER = 'bVEzazo4QTZi';// Example: base64 decodes to "mQ3k:8A6b" -> XOR key = "mQ3k"// The raw POST body (base64 string from the request):const CAPTURED_BODY = 'S3VHcGpXT1FaUFBVUFFUUVZZV1VUUVZaUVlZWVVQUFZXWVRRWFBVUVZWVQ==';// Example body encoded with key "mQ3k"// ─────────────────────────────────────────────────────────────────────────────/** * XOR cipher — exact match of SetupVPN's Eo() function in background.bundle.js. * Both text and key are plain JS strings. The key repeats cyclically. */function xorCipher(text, key) { const textChars = text.split(''); const keyChars = key.split(''); for (let i = 0; i < textChars.length; i++) { textChars[i] = String.fromCharCode( textChars[i].charCodeAt(0) ^ keyChars[i % keyChars.length].charCodeAt(0) ); } return textChars.join('');}/** * Extract the XOR key from the Authorization: Basic header value. * Header format (after base64-decoding): "<xorKey>:<randomSuffix>" * Only the part before the first colon is the XOR key. */function extractKey(authHeaderValue) { const b64 = authHeaderValue.replace(/^Basic\s+/i, '').trim(); const decoded = Buffer.from(b64, 'base64').toString('utf8'); const key = decoded.split(':')[0]; if (!key) throw new Error('Could not extract XOR key from Authorization header'); return key;}/** * Decode a SetupVPN POST body. * Mirrors SetupVPN's wo() function: atob -> unescape(escape(s)) -> XOR * * The unescape/escape step converts latin-1 byte sequences (produced after * XOR) back to valid UTF-8 — required for any non-ASCII bytes in the params. */function decodeBody(encodedBody, xorKey) { // base64-decode to binary string (latin-1 byte values) const rawBytes = Buffer.from(encodedBody, 'base64').toString('binary'); // unescape(escape(s)) equivalent: percent-encode each byte, then decode as UTF-8 const unescaped = decodeURIComponent(encodeURIComponent(rawBytes)); return xorCipher(unescaped, xorKey);}/** * Parse URL-encoded query string into a plain object. */function parseQueryString(qs) { const params = {}; for (const part of qs.split('&')) { if (!part) continue; const eqIdx = part.indexOf('='); if (eqIdx === -1) { params[decodeURIComponent(part)] = ''; continue; } const k = decodeURIComponent(part.slice(0, eqIdx)); const v = decodeURIComponent(part.slice(eqIdx + 1)); params[k] = v; } return params;}// ─── Main ─────────────────────────────────────────────────────────────────────let xorKey;try { xorKey = extractKey(CAPTURED_AUTH_HEADER);} catch (err) { console.error('ERROR extracting XOR key:', err.message); console.error('Make sure CAPTURED_AUTH_HEADER is the base64 value from the Authorization header,'); console.error('NOT including the "Basic " prefix — or paste the full header value and the script'); console.error('will strip the prefix automatically.'); process.exit(1);}console.log('Authorization header : ' + CAPTURED_AUTH_HEADER);console.log('Decoded header value : ' + Buffer.from(CAPTURED_AUTH_HEADER.replace(/^Basic\s+/i,''), 'base64').toString('utf8'));console.log('XOR key extracted : ' + JSON.stringify(xorKey));console.log('');let decoded;try { decoded = decodeBody(CAPTURED_BODY, xorKey);} catch (err) { console.error('ERROR decoding body:', err.message); console.error('Make sure CAPTURED_BODY is the raw base64 string from the POST body.'); process.exit(1);}console.log('Decoded query string:');console.log(decoded);console.log('');let params;try { params = parseQueryString(decoded);} catch (err) { console.error('Parsed as raw string only (not URL-encoded params):', err.message); params = null;}if (params && Object.keys(params).length > 0) { console.log('Parsed params:'); const maxKeyLen = Math.max(...Object.keys(params).map(k => k.length)); for (const [k, v] of Object.entries(params)) { console.log(' ' + k.padEnd(maxKeyLen + 2) + '= ' + v); }} else if (params !== null) { console.log('(No URL-encoded params found — decoded value shown above is the plaintext)');}
How to run it
  1. 1node setupvpn-xor-decode.js
Hardcoded keys found in main.wasm
  • Hardcoded AES key / seed
    JHn7tK7OjOWMHHPm2wEgeS3E8Gbq3PPgZx3dnj3Xx?q!!

    A 44-character string baked directly into the compiled WASM binary, cannot be changed without releasing a new extension version.

  • Secondary key candidate
    X2skj39mcLmc2!js

    A 16-character string adjacent to AES symbols in the WASM data segment, matches standard AES-128 key length.

What it can do

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

  • Read and change your data on every site you visit

    <all_urls>

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

    proxy

  • Store data in your browser

    storage

  • Watch every request your browser makes

    webRequest

  • Show you desktop notifications

    notifications

  • See the address and title of every tab you have open

    tabs

  • See, disable and uninstall your other extensions, including your security ones

    management

  • Schedule its own background tasks

    alarms

  • Block and redirect the requests your browser makes

    declarativeNetRequest

webRequestAuthProvider
Updated 30 September 2026oofgbpoabipfcfjapgnbbjjaenockbdp