Is ВПН для Телеграм / VPN for Telegram safe?

High risk

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.

katia.noomovav2.1Chrome Web Store
72Risk
Who publishes it

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.

Publisher
katia.noomova

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.

katnmv.github.io
Also called by 1 other listing: TexAu v2
mainapi.store
Also called by 1 other listing: TexAu v2
twexst.blogspot.com
Also called by 1 other listing: TexAu v2

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
What actually happens
You did this

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 did this

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.

Captured request
GEThttps://katnmv.github.io/exs/list.json

200 OK configuration response observed during dynamic analysis.

What a PAC-controlled proxy path can receive
  • Requested destination
    https://accounts.google.com/signin (illustrative)

    A proxy rule can route the sites you visit through a proxy chosen by the configuration source.

  • Request headers
    User-Agent: Mozilla/5.0 (illustrative)

    Headers can describe your browser and, on inspectable traffic, site-specific request context.

  • Proxy selection
    PROXY 154.221.55.116:62800

    The remote PAC script decides whether a destination goes direct or through a proxy path.

The code that does this

Remote entries become PAC script data when they are not host:port values

Readable version

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();  }}
Remote configuration sources used by the extension
    • 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.

Updated 30 September 2026akdkpiglbicjkebnjgohheaecgohjall