Is TikTok Downloader, Sort & Analytics | KOLSprite safe?

High risk

KOLSprite is high risk. KOLSprite injects a page-world script into tiktok.com tabs, overriding fetch/XHR to watch its API responses. Browsing TikTok, it collects, half the time sending records to plugin-data.kolsprite.com. A capture held 13: IDs, handles, stats.…

Yunya Technologyv2.2.1Chrome Web Store
75Risk
Who publishes it

Chengdu Yunya Information Technoloy Co.,LTD - 2 other listings from the same operator, 1 of them carrying a finding

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
Yunya Technology
Declared legal entity
Chengdu Yunya Information Technoloy Co.,LTD
Registered address
No.200 Tianfu Street 5, Gaoxinqu, Chengu, Sichuan 610041, China
Registered contact
chen zhiwu

Same store account

2 other listings published from this account, 510k+ users between them. 1 of them carries a finding.

Shared hosts - 2 hostnames

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.

o.kolsprite.com
Also called by 6 other listings, including TikTok Cleaner | Remove Likes & Reposts with One-Click
kolsprite.com
Also called by 8 other listings, including TikTok Cleaner | Remove Likes & Reposts with One-Click, Check Engagement Rate Instagram & Tiktok by KOL.ID, SellerSprite - The Most Professional Amazon Tool

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

Publishers can request a review.

Findings

TikTok feed data relayed to a KOLSprite analytics endpoint

KOLSprite injects a page-world script into tiktok.com tabs, overriding fetch/XHR to watch its API responses.

Browsing TikTok, it collects, half the time sending records to plugin-data.kolsprite.com.

A capture held 13: IDs, handles, stats.

Severity
High unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You open TikTok and scroll the home feed, run a search, or view a creator's videos.

TikTok's own web app requests the matching video-list data from its API as you browse.

The extension did this

The extension's injected script intercepts that TikTok API response and relays the video data to the extension, which then forwards it to KOLSprite's own server.

The relay runs regardless of whether you use any KOLSprite-branded feature on the page.

Captured request
POSThttps://plugin-data.kolsprite.com/v1/plugin/video/label/add

Dynamic analysis observed a 90,406-byte JSON array of 13 video objects in the request body during a TikTok browsing session; the object above reproduces the confirmed field shape for one element (video id and author.uniqueId are the exact captured values, other field values are illustrative since the harness recorded field matches rather than a full body transcript).

Headers
Content-Type
application/json
Body
[  {    "id": "7637649207146368269",    "desc": "(illustrative) video caption text",    "createTime": 1752345600,    "author": {      "id": "6829112233445566778",      "uniqueId": "coolestbabeoutthere",      "nickname": "(illustrative) display name"    },    "statsV2": {      "diggCount": "128400",      "playCount": "2104300",      "shareCount": "3210",      "commentCount": "987",      "collectCount": "411"    },    "video": {      "id": "7637649207146368269",      "duration": 27    }  }  // ...12 more video objects in the captured batch]
Fields present in the request sent to plugin-data.kolsprite.com
  • TikTok video ID
    7637649207146368269

    Identifies the exact video you viewed or scrolled past.

  • Video creator's TikTok handle
    coolestbabeoutthere

    Names the account that posted each video appearing in your feed, search, or profile view.

  • Engagement stats
    diggCount, playCount, shareCount, commentCount, collectCount

    Sends the like, view, share, comment, and save counts shown for each video.

  • Video post time
    createTime: 1752345600

    Sends when each video was originally posted.

The code that does this

Fetch/XHR interception in the injected page script; forwarding call in the extension bundle

Readable version

inject.ts.js — capture table and fetch override (renamed)

assets/inject.ts.js:1-21,87-124
window.__kol_captureList = {  "/api/recommend/item_list/": "foryouVideoList",  "/api/post/item_list": "userVideoList",  "/api/repost/item_list": "userRepostList",  "/api/favorite/item_list": "userLikeList",  "/api/collection/item_list": "userFavoriteList",  "/api/user/collect/item_list/": "userPageFavoriteList",  "/api/search/general/full": "searchGeneralList",  "/api/search/item/full/": "searchVideoList",  "/api/explore/item_list/": "exploreVideoList",  "/api/challenge/item_list/": "tagVideoList",  "/api/following/item_list": "followingVideoList",  "/api/friends/item_list": "friendsVideoList",  "/api/music/item_list": "musicVideoList",  "/api/item/detail": "userVideoDetail",  "/api/shop/brandy_desktop/tiktok-ug": "relatedList",  "/api/shop": "shopList",  "/api/user/list/": "fansList",  "/api/comment/list/": "commentLsit",  "/api/effect/item/list/": "effectList"};function installFetchHook() {  const capturePaths = Object.keys(window.__kol_captureList);  const originalFetch = window.fetch;  window.fetch = async function (...args) {    let url = "";    const resource = args[0], init = args[1];    if (typeof resource === "string") {      url = resource;    } else if (resource instanceof URL) {      url = resource.href;    } else if (resource instanceof Request) {      url = resource.url;    }    let matchedPath;    for (const path of capturePaths) {      if (url.includes(path)) { matchedPath = path; break; }    }    if (!matchedPath) return originalFetch.apply(this, args);    const response = await originalFetch.apply(this, args);    const contentType = response.headers.get("Content-Type") || "";    if (response.ok && contentType.includes("application/json")) {      response.clone().text().then((text) => {        if (!text) return;        queueMicrotask(() => {          try {            const json = JSON.parse(text);            dispatchBuffered(new CustomEvent(window.__kol_captureList[matchedPath], { detail: json }));          } catch {}        });      });    }    return response;  };}window.__load_kol_main = true;installFetchHook();

index.js — forwarding call, gated at ~50% (renamed)

assets/index.js:101514,101544-101560
const KOL_API_BASE = "https://plugin-data.kolsprite.com/v1/plugin";function sendVideoLabelBatch(videos = [], region = "") {  if (Math.random() > 0.5) return; // ~50% sampling  let cleaned = lodashCloneDeep(videos);  cleaned    .filter((item) => (item?.authorStats?.followerCount) >= 2000)    .forEach((item) => {      if (item?.video) {        delete item.video.zoomCover;        delete item.video.bitrateInfo;        delete item.video.claInfo;        delete item.video.PlayAddrStruct;        delete item.video.subtitleInfos;      }      delete item.music;      delete item.challenges;      delete item.textExtra;    });  const url = region    ? `${KOL_API_BASE}/video/label/add?region=${region}`    : `${KOL_API_BASE}/video/label/add`;  return httpClient(url, { method: "post", body: cleaned, hide: true, noBaseUrl: true });}function forwardVideoBatch(videos, region = "") {  sendVideoLabelBatch(videos, region);}
Where the intercepted TikTok data is sent
    • plugin-data.kolsprite.com

    Receives batches of TikTok video records (ID, creator handle, post time, engagement stats) from your own browsing; operated by publisher KOLSprite.

TikTok creator-list data copied into extension storage

KOLSprite's injected script intercepts TikTok's /api/user/list/ during creator search.

We captured 3 responses (56KB each): full user records, handle, name, ID, follower counts.

Copy Search sends handles to kolsprite.com as a URL parameter.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You open a TikTok profile and use the extension's recommended-creator search or its 'Export Followers' control.

TikTok's own web app requests the matching user list from its /api/user/list/ API.

The extension did this

The extension's injected script intercepts that response and copies the returned TikTok user records into its own in-memory store.

The stored records include each account's handle, display name, follower/following counts, and region.

Captured request
GEThttps://www.tiktok.com/api/user/list/?count=30&scene=151&secUid=MS4wLjABAAAA0PbA1r_Z7yqk9dTgHVjZQxKz1e3rC2s4tYk8fH0nWmE&aid=1988

Dynamic analysis captured 3 GET requests to this endpoint (56,046-56,052 byte JSON bodies) immediately after clicking the extension's 'Export Followers' control on a TikTok creator profile. The scene=151 query parameter and the response field shape below were directly observed; the other TikTok-standard query parameters shown (count, secUid, aid) are illustrative reconstructions of the query string the extension's own code reads, not individually logged captures.

Body
[  {    "uniqueId": "traveldiaries.mia",    "nickname": "Mia | Travel Diaries",    "secUid": "MS4wLjABAAAA0PbA1r_Z7yqk9dTgHVjZQxKz1e3rC2s4tYk8fH0nWmE",    "followerCount": 184300,    "followingCount": 612,    "diggCount": 92100,    "videoCount": 341,    "heartCount": 2540000,    "friendCount": 12,    "region": "US",    "verified": false,    "privateAccount": false,    "relation": 0  }  // ...29 more creator records in the same response (illustrative values; the harness recorded field matches rather than a full body transcript)]
Fields present in each captured TikTok user record
  • TikTok handle
    traveldiaries.mia

    Identifies the exact TikTok account in the recommended-creator list.

  • TikTok internal account ID (secUid)
    MS4wLjABAAAA0PbA1r_Z7yqk9dTgHVjZQxKz1e3rC2s4tYk8fH0nWmE

    A TikTok-issued opaque ID tied to one specific account, usable to look that account up directly through TikTok's API.

  • Follower and engagement counts
    followerCount: 184300, followingCount: 612, diggCount: 92100, videoCount: 341, heartCount: 2540000

    Copies the follower count, following count, and total likes/videos for each account in the list.

  • Account region
    region: US

    Copies the country/region TikTok has on file for each account.

  • Account status flags
    verified: false, privateAccount: false, relation: 0

    Copies whether each account is verified, set to private, or already follows/is followed by you.

The code that does this

The fetch hook that captures /api/user/list/ responses, and the handler that stores them

Readable version

inject.ts.js — fetch override matching /api/user/list/ and attaching scene/query params (renamed)

assets/inject.ts.js
function installFetchAndXhrHooks() {  const fetchCapturePaths = Object.keys(window.__kol_captureList);  const xhrCapturePaths = Object.keys(window.__kol_captureList_xhr);  const originalFetch = window.fetch;  window.fetch = async function (...args) {    let url = "", credentials, headers;    const resource = args[0], init = args[1];    if (typeof resource === "string") {      url = resource; credentials = init?.credentials; headers = init?.headers;    } else if (resource instanceof URL) {      url = resource.href; credentials = init?.credentials; headers = init?.headers;    } else if (resource instanceof Request) {      url = resource.url; credentials = resource.credentials; headers = resource.headers;    }    const isInternalRequest =      (headers instanceof Headers && headers.get("x-kol-internal") === "1") ||      headers?.["x-kol-internal"] === "1" ||      headers?.["X-Kol-Internal"] === "1";    if (credentials === "omit" || isInternalRequest) {      if (isInternalRequest && init?.headers) {        const strippedHeaders = { ...init.headers };        delete strippedHeaders["x-kol-internal"];        delete strippedHeaders["X-Kol-Internal"];        return originalFetch.call(this, resource, { ...init, headers: strippedHeaders });      }      if (isInternalRequest && resource instanceof Request) {        const strippedHeaders = new Headers(resource.headers);        strippedHeaders.delete("x-kol-internal");        return originalFetch.call(this, new Request(resource, { headers: strippedHeaders }));      }      return originalFetch.apply(this, args);    }    let matchedPath;    for (let i = 0; i < fetchCapturePaths.length; i++) {      const path = fetchCapturePaths[i];      if (url.includes(path)) { matchedPath = path; break; }    }    if (!matchedPath) return originalFetch.apply(this, args);    const response = await originalFetch.apply(this, args);    const contentType = response.headers.get("Content-Type") || "";    if (response.ok && contentType.includes("application/json")) {      response.clone().text().then((text) => {        if (!text) return;        queueMicrotask(() => {          try {            const json = JSON.parse(text);            const query = new URLSearchParams(url);            if (query.get("post_item_list_request_type")) {              const t = query.get("post_item_list_request_type");              json.tkOrder = !t || t === "0" ? "Latest" : t === "1" ? "Popular" : "Oldest";            }            let payload = json;            if (matchedPath === "/api/user/list/") {              const params = {};              new URLSearchParams(url.split("?")[1] || "").forEach((value, key) => { params[key] = value; });              payload = { ...json, params };            }            dispatchBuffered(new CustomEvent(window.__kol_captureList[matchedPath], { detail: payload }));            if (window.__kol_captureList_urlReferer[matchedPath]) {              dispatchBuffered(new CustomEvent("urlRefererGet", { detail: url }));            }          } catch {}        });      });    }    return response;  };  const xhrProto = XMLHttpRequest.prototype;  const originalOpen = xhrProto.open;  const originalSend = xhrProto.send;  const xhrUrls = new WeakMap();  xhrProto.open = function (method, url, ...rest) {    try { if (typeof url === "string") xhrUrls.set(this, url); } catch {}    return originalOpen.call(this, method, url, ...rest);  };  xhrProto.send = function (...args) {    const url = xhrUrls.get(this) || "";    let matchedPath;    for (let i = 0; i < xhrCapturePaths.length; i++) {      const path = xhrCapturePaths[i];      if (url.includes(path)) { matchedPath = path; break; }    }    if (matchedPath) {      this.addEventListener("loadend", () => {        try {          const text = this.responseText;          if (!text) return;          const json = JSON.parse(text);          dispatchBuffered(new CustomEvent(window.__kol_captureList_xhr[matchedPath], { detail: { data: json, fetchURL: url } }));        } catch {}      }, { once: true });    }    return originalSend.apply(this, args);  };  const cachedFeed = window.localStorage.getItem("fyp-feed-cache");  if (cachedFeed) {    queueMicrotask(() => {      try { dispatchBuffered(new CustomEvent("foryouVideoList", { detail: JSON.parse(cachedFeed) })); } catch {}    });  }  queueMicrotask(() => { dispatchBuffered(new CustomEvent("refreshUserProfile")); });  console.log("inject success");}window.__load_kol_main = true;installFetchAndXhrHooks();

content.ts.js — routing captured user-list records by scene (renamed)

assets/content.ts.js
function handleFansListCapture(event) {  const userList = event.detail.userList;  const params = event.detail.params || {};  const { addFans, setFansTotal, addFollow, setFollowTotal, addRecommend, setRecommendTotal } =    fansStore.getState();  const hasRecords = userList && Array.isArray(userList) && params.count !== "5";  if (!hasRecords) return;  if (params.scene === "21") {    // followers scene reaches here, but no store update is invoked    return;  }  if (params.scene === "151") {    addRecommend(userList);    setRecommendTotal(event.detail.total);  }  // any other scene (e.g. "67" for following) reaches here with no store update}window.addEventListener("fansList", handleFansListCapture);
What's stored on your device

Holds recommended-creator records captured this session. fanslist/followlist exist but unused here. Exportable as CSV from the panel.

Location
The extension's in-memory Zustand store (recommendlist, fanslist, followlist keys), created in index.js and shared with the content script
Contents
{  "fanslist": [],  "followlist": [],  "recommendlist": [    {      "region": "US",      "nickname": "Mia | Travel Diaries",      "uniqueId": "traveldiaries.mia",      "followerCount": 184300    }  ],  "recommendTotal": 30}
Where captured creator handles can leave the device
    • www.kolsprite.com

    Receives recommended-creator TikTok handles as a URL parameter when you click 'Copy Search'; opened in a new tab, not sent in the background. Operated by KOLSprite.

Updated 30 September 2026penohjgblobinadflplocjekaclnoemh