Is ТурбоМинимум RM - статистика рекламы на WB safe?
ТурбоМинимум RM - статистика рекламы на WB is critical risk. After finding a Wildberries seller token in storage, the API wrapper reuses it as an Authorizev3 header on Seller Alert sync requests. Portrait/advert-stat calls POST supplier and campaign data to api.seller-alert.com with that token.
Who publishes itSTATGROUP - no other listings under this identity, 1 shared hostname
STATGROUP - no other listings under this identity, 1 shared hostname
What this publisher told the store about itself, and the other listings that told it the same thing.
Shared hosts - 1 hostname
Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Wildberries token added to Seller Alert requests
After finding a Wildberries seller token in storage, the API wrapper reuses it as an Authorizev3 header on Seller Alert sync requests.
Portrait/advert-stat calls POST supplier and campaign data to api.seller-alert.com with that token.
- Severity
- Critical unwanted
- Type
- Unexpected
- CWE
- CWE-522
- Source
- Dynamic sandbox
You use the extension on Wildberries seller or advertising pages.
The relevant extension features include the Seller Alert portrait and advert-stat sync paths.
The extension reuses the Wildberries access token as an Authorizev3 header on Seller Alert API requests.
The token is read from browser storage, cached by the background script, and returned to the page API wrapper before the POST runs.
- Wildberries access tokenAuthorizev3: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5ODc2NTQzMjEiLCJzdXBwbGllcl9pZCI6IjEyMzQ1NiJ9.q7lKqQsuhqNlk8yCkYaOQw (illustrative)
This credential can authenticate requests tied to your Wildberries seller account.
- Storage token keysaccess-token-web, access-token, wb-eu-passport-v2.access-token
The extension checks browser storage locations where your Wildberries session token may be kept.
- Extension cache keyturbominimum_rm_seller_token
The background script keeps the token under its own storage key so later page code can request it.
- Seller Alert portrait payloadsupplier_id=123456, date=2024-01-15, data=[portrait rows] (illustrative)
The portrait sync sends supplier, date, and portrait records together with the credential-bearing header.
- Seller Alert advert-stat payloadsupplier_id=123456, date=2024-01-15, campaigns=[campaign rows] (illustrative)
The advert-stat sync sends supplier, date, and campaign records together with the credential-bearing header.
The extension copies the token from the Wildberries page context into extension storage before using it on later API calls.
- Location
- Wildberries page localStorage and chrome.storage.local
{ "pageTokenKeys": [ "access-token-web", "access-token", "wb-eu-passport-v2.access-token" ], "backgroundTokenKey": "turbominimum_rm_seller_token", "messageToContentScript": { "type": "TOKEN_REFRESHED", "token": "redacted Wildberries access token" }, "messageFromContentScript": { "type": "TOKEN_UPDATE", "token": "redacted Wildberries access token" }}The source handles JSON responses and explicit 404, 429, and non-OK error cases.
- Authorizev3
- redacted Wildberries access token
- Content-Type
- application/json
- extension_version
- 64
The source reads the response as JSON after the wrapped request completes.
- Authorizev3
- redacted Wildberries access token
- Content-Type
- application/json
- extension_version
- 64
Shipped code path that reads the token and adds it to Seller Alert fetches
Content token sync
src/utils/cmp-token-sync.jsconst tokenKeys = window.AppConstants && window.AppConstants.WB_TOKEN_STORAGE_KEYS || [];const canUseRuntime = window.isChromeRuntimeAvailable;let lastToken = null;let intervalId = null;function readWildberriesToken() { for (const key of tokenKeys) { const token = localStorage.getItem(key) ?? null; if (token && typeof token === 'string' && token.length > 0) { return token; } } return null;}function syncTokenToBackground() { const token = readWildberriesToken(); if (token !== lastToken) { lastToken = token; chrome.runtime.sendMessage({ type: 'TOKEN_UPDATE', token }, () => { if (chrome.runtime.lastError) { console.warn('[CMP-Token-Sync]', 'Failed to send token to background:', JSON.stringify({ error: chrome.runtime.lastError.message })); } }); }}function watchTokenChanges() { const originalSetItem = Storage.prototype.setItem; let pending = false; Storage.prototype.setItem = function(key, value) { const result = originalSetItem.apply(this, arguments); if (!pending && tokenKeys.includes(key)) { pending = true; setTimeout(() => { syncTokenToBackground(); pending = false; }, 0); } return result; }; window.addEventListener('storage', event => { if (event.key && tokenKeys.includes(event.key)) { syncTokenToBackground(); } }); intervalId = setInterval(() => { syncTokenToBackground(); }, 2000);}if (!tokenKeys || tokenKeys.length === 0 || !canUseRuntime) { console.warn('[CMP-Token-Sync] WB_TOKEN_STORAGE_KEYS is not available or empty');} else if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { syncTokenToBackground(); watchTokenChanges(); });} else { syncTokenToBackground(); watchTokenChanges();}window.addEventListener('beforeunload', () => { if (intervalId !== null) { clearInterval(intervalId); intervalId = null; }});Background token cache and page Auth lookup
src/background/token-sync-utils.js and src/api/auth/api.jsconst storageKey = window.AppConstants.CACHE_PREFIXES.SELLER_TOKEN_STORAGE_KEY();let currentToken = null;function loadTokenFromStorage() { chrome.storage.local.get([storageKey], values => { currentToken = values[storageKey] || null; });}function setToken(token) { const nextToken = token || null; if (nextToken !== currentToken) { currentToken = nextToken; chrome.storage.local.set({ [storageKey]: nextToken }); }}function getCurrentToken() { return currentToken;}chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message && message.type === 'TOKEN_UPDATE') { setToken(message.token || null); sendResponse({ success: true }); return true; } if (message && message.type === 'TOKEN_GET') { sendResponse({ token: getCurrentToken() }); return true; }});async function getAccessToken() { return await new Promise(resolve => { chrome.runtime.sendMessage({ type: 'TOKEN_GET' }, response => { if (chrome.runtime.lastError) { resolve(null); return; } const token = response && typeof response.token === 'string' && response.token.length > 0 ? response.token : null; resolve(token); }); });}API wrapper adds Authorizev3 to requests
src/api/api-request-wrapper.jsasync function getAccessTokenForWrapper() { if (!window.Auth || typeof window.Auth.getAccessToken !== 'function') { return null; } try { return await window.Auth.getAccessToken(); } catch (error) { return null; }}function addAuthorizev3(headers, token) { if (!token) { return headers; } if (headers instanceof Headers) { if (!headers.has('Authorizev3')) { headers.set('Authorizev3', token); } return headers; } if (headers && typeof headers === 'object') { if (!headers.Authorizev3 && !headers.authorizev3) { headers.Authorizev3 = token; } return headers; } return headers;}function createApiRequest(callback, options = {}) { return async function(...args) { const token = await getAccessTokenForWrapper(); const originalFetch = window.fetch; let fetchWasPatched = false; if (token && typeof window !== 'undefined' && window.fetch) { window.fetch = function(url, init) { if (init && init.headers) { init.headers = addAuthorizev3(init.headers, token); } else if (init) { init.headers = addAuthorizev3({}, token); } else { init = { headers: addAuthorizev3({}, token) }; } return originalFetch(url, init); }; fetchWasPatched = true; } try { return await callback(...args); } finally { if (fetchWasPatched) { window.fetch = originalFetch; } } };}Seller Alert portrait and advert-stat POSTs
src/api/integration-portrait/api.js and src/api/integration-rk-stats/api.jsasync function sendPortraitData(request) { const { supplier_id, date, data, extension_version } = request || {}; if (!supplier_id || !date || !Array.isArray(data) || data.length === 0) { throw new Error('supplier_id, date and data are required'); } const payload = { supplier_id: String(supplier_id), date: String(date), data }; const runRequest = window.APIRequestWrapper.createApiRequest(async () => { const headers = { 'Content-Type': 'application/json', extension_version }; return await fetch(window.AppConstants.URLS.SELLER_ALERT_PORTRAIT(), { method: 'POST', headers, body: JSON.stringify(payload) }); }, { requestKey: 'integration-portrait-' + payload.supplier_id + '-' + payload.date }); return await runRequest();}async function sendRKStatsPerDay(request) { const { supplier_id, date, campaigns, extension_version, hasRetry = true, onRetry, statusHandlers } = request || {}; if (!supplier_id || !date || !Array.isArray(campaigns) || campaigns.length === 0) { throw new Error('supplier_id, date and campaigns are required'); } const payload = { supplier_id: String(supplier_id), date: String(date), campaigns }; const runRequest = window.APIRequestWrapper.createApiRequest(async () => { const headers = { 'Content-Type': 'application/json', extension_version }; return await fetch(window.AppConstants.URLS.SELLER_ALERT_ADVERT_STATS(), { method: 'POST', headers, body: JSON.stringify(payload) }); }, { requestKey: 'integration-rk-stats-perday-' + supplier_id + '-' + date, hasRetry, onRetry, statusHandlers }); const response = await runRequest(); return await response.json();}- api.seller-alert.com
Receives extension portrait and advert-stat POST requests; the code path adds the Wildberries Authorizev3 header when a token is cached.