Is ВПН для Телеграм / VPN for Telegram safe?
VPN for Telegram loads remote proxy settings that can replace its PAC script for all browser destinations.
The extension has proxy permission and access to all URLs, then fetches proxy configuration from remote sources. That configuration can provide a PAC script, allowing remotely supplied rules to route browser traffic beyond Telegram through a selected proxy.
Who publishes itkatia.noomova - 7 other listings from the same operator, 1 of them carrying a finding
katia.noomova - 7 other listings 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
7 other listings published from this account, 34k+ users between them. 1 of them carries a finding.
Shared hosts - 3 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
Remote Config Can Proxy Browser Traffic
VPN for Telegram fetches proxy config from katnmv.github.io/exs/list.json (200 response in testing).
It decodes entries; a non host:port entry is installed as PAC data, letting that source control routing.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-829
- Source
- Dynamic sandbox
You connect through one of the servers shown in the popup.
The popup loads a remote server list before it builds that server menu.
The extension can install a PAC script supplied through that remote list.
If the selected entry is not a normal host and numeric port, the source code treats the full entry as proxy script data.
200 OK configuration response observed during dynamic analysis.
- Requested destinationhttps://accounts.google.com/signin (illustrative)
A proxy rule can route the sites you visit through a proxy chosen by the configuration source.
- Request headersUser-Agent: Mozilla/5.0 (illustrative)
Headers can describe your browser and, on inspectable traffic, site-specific request context.
- Proxy selectionPROXY 154.221.55.116:62800
The remote PAC script decides whether a destination goes direct or through a proxy path.
Remote entries become PAC script data when they are not host:port values
Fetch the first reachable remote config URL
async function fetchConfigWithTimeout(url, timeoutMs = 5000) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { headers: { "Cache-Control": "no-cache" }, signal: controller.signal }); clearTimeout(timeoutId); lastConfigUrl = url; if (!response.ok) throw new Error(`HTTP ${response.status}`); const text = await response.text(); if (!text) throw new Error("Empty response"); return text; } catch (error) { clearTimeout(timeoutId); throw error; }}async function getConfigData() { const urls = [configUrls.github, configUrls.blogspot, configUrls.googleDoc]; for (const url of urls) { try { return await fetchConfigWithTimeout(url); } catch (error) { console.warn("Error fetch:", error.message); } } throw new Error("No data");}Decode remote entries and store the raw server list
function decodeConfigJson(data) { if (typeof data === "undefined") return; try { const decodedText = caesarShiftBack(data.trim(), 3); const config = JSON.parse(decodedText); if (config.u && config.u.length > 0) { checkoutUrl = "https://" + config.u; chrome.storage.local.set({ apiurl: config.u }); } if (config.s) { chrome.storage.local.set({ servers: JSON.stringify(config.s) }); const proxyEntries = []; const displayLabels = []; config.s.forEach((entry) => { const parts = atob(entry).split("|"); const countryIsBlocked = parts[2] && parts[2].length > 0 && bannedCountries.includes(parts[2].trim()); if (!countryIsBlocked) { proxyEntries.push(parts[0]); displayLabels.push(parts[1] ? parts[1] : "(Free)"); } }); serverConfigs.unshift(...proxyEntries); listServers.unshift(...displayLabels); freeServerCount = serverConfigs.length; addServersToPopup(); finishPopupLoading(); } } catch (error) { finishPopupLoading(); loadLastConfig(); }}function caesarShiftBack(text, shift) { return text.split("").map((char) => { if (char.match(/[a-z]/i)) { const code = char.charCodeAt(0); const offset = code >= 65 && code <= 90 ? 65 : 97; return String.fromCharCode(((code - offset - shift + 26) % 26) + offset); } return char; }).join("");}Return full PAC script data when the entry is not host:port
async function getSelectedProxyData() { try { if (selectedProxyId === "vip") { const result = await new Promise((resolve) => chrome.storage.local.get(["vipserver"], resolve)); if (result.vipserver) { const data = JSON.parse(result.vipserver); chrome.storage.local.set({ dataonAuthRequired: JSON.stringify(data) }); return { host: data.host, port: data.port, login: data.user, pass: data.pass }; } } else { const result = await new Promise((resolve) => chrome.storage.local.get(["servers"], resolve)); if (result.servers) { const storedEntries = JSON.parse(result.servers); const remoteProxyEntries = []; storedEntries.forEach((entry) => { const parts = atob(entry).split("|"); const countryIsBlocked = parts[2] && parts[2].length > 0 && bannedCountries.includes(parts[2].trim()); if (!countryIsBlocked) remoteProxyEntries.push(parts[0]); }); allProxyEntries = remoteProxyEntries.concat(serverConfigs); } const selectedEntry = allProxyEntries[selectedProxyId - 1]; if (selectedEntry) { const [host, port, login, pass] = selectedEntry.split(":"); if (host && /^\d+$/.test(port)) { const proxy = { host, port, login, pass }; chrome.storage.local.set({ dataonAuthRequired: JSON.stringify(proxy) }); return proxy; } return { full: selectedEntry }; } } } catch (error) { console.error(error); } return null;}Install the selected entry as the active PAC script
async function installProxy(proxy) { try { let pacData = proxy.full ? proxy.full : bundledTelegramPac; if (/^socks/isu.test(proxy.host)) { pacData = pacData.replace("PROXY {{host}}", proxy.host).replace("{{port}}", proxy.port); } else if (/^http/isu.test(proxy.host)) { pacData = pacData.replace("PROXY {{host}}", proxy.host).replace("{{port}}", proxy.port); } else if (proxy.port == 443) { pacData = pacData.replace("PROXY {{host}}", "HTTPS " + proxy.host).replace("{{port}}", proxy.port); } else { pacData = pacData.replace("{{host}}", proxy.host).replace("{{port}}", proxy.port); } if (!pacData.includes("FindProxyForURL")) { pacData = bundledTelegramPac.replace("PROXY {{host}}:{{port}}", pacData); } await chrome.proxy.settings.set({ value: { mode: "pac_script", pacScript: { data: pacData } }, scope: "regular" }); setIcon(true); chrome.storage.local.set({ workstatus: "on" }); } catch (error) { console.error(error); turnProxyOff(); }}- katnmv.github.io
Primary server-list endpoint observed returning a 200 configuration response.
- twexst.blogspot.com
Fallback server-list endpoint used if the primary fetch fails.
- docs.google.com
Fallback document endpoint parsed for server-list content if earlier fetches fail.
Where it sends data
Destinations our analysis observed ВПН для Телеграм / VPN for Telegram 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.
- katnmv.github.io
ВПН для Телеграм / VPN for Telegram sends data to katnmv.github.io. One other extension we have analysed sends data here.