Is TikTok Downloader, Sort & Analytics | KOLSprite safe?
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.…
Who publishes itChengdu Yunya Information Technoloy Co.,LTD - 2 other listings from the same operator, 1 of them carrying a finding
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.
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.
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
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'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.
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).
- Content-Type
- application/json
[ { "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]- TikTok video ID7637649207146368269
Identifies the exact video you viewed or scrolled past.
- Video creator's TikTok handlecoolestbabeoutthere
Names the account that posted each video appearing in your feed, search, or profile view.
- Engagement statsdiggCount, playCount, shareCount, commentCount, collectCount
Sends the like, view, share, comment, and save counts shown for each video.
- Video post timecreateTime: 1752345600
Sends when each video was originally posted.
Fetch/XHR interception in the injected page script; forwarding call in the extension bundle
inject.ts.js — capture table and fetch override (renamed)
assets/inject.ts.js:1-21,87-124window.__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-101560const 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);}- 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
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'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.
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.
[ { "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)]- TikTok handletraveldiaries.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 countsfollowerCount: 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 regionregion: US
Copies the country/region TikTok has on file for each account.
- Account status flagsverified: false, privateAccount: false, relation: 0
Copies whether each account is verified, set to private, or already follows/is followed by you.
The fetch hook that captures /api/user/list/ responses, and the handler that stores them
inject.ts.js — fetch override matching /api/user/list/ and attaching scene/query params (renamed)
assets/inject.ts.jsfunction 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.jsfunction 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);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
{ "fanslist": [], "followlist": [], "recommendlist": [ { "region": "US", "nickname": "Mia | Travel Diaries", "uniqueId": "traveldiaries.mia", "followerCount": 184300 } ], "recommendTotal": 30}- 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.