Is ClearTok - TikTok Repost Remover safe?

Medium risk

ClearTok sends your TikTok username and repost-removal activity to its own servers via a session-based tracking system.

Each time you use the extension, it creates a session tied to your TikTok username and reports events — including when you start and complete a repost-removal run, how many reposts were removed, and any errors encountered — to api.cleartok.io. Removal counts are transmitted with your cleartok.io account cookies attached, linking your usage to your account. The extension also fetches its DOM selector configuration remotely every six hours, allowing the operator to update which buttons the extension clicks on TikTok without a new extension release.

auroroav1.8.8Chrome Web Store
49Risk
Who publishes it

Personal - no other listings under this identity

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

Publisher
auroroa
Declared legal entity
Personal
Registered address
79 South River Road, Willowick, NH 03110, US
Registered contact
clear

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

Publishers can request a review.

Findings

Remote selector updates change TikTok DOM actions

ClearTok requests selector metadata from api.cleartok.io and gets a selectors object, stored locally to find TikTok profile, repost-tab, repost-button, and video-list elements.

Refreshed on install, startup, and after six hours.

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

The extension reaches a selector refresh point.

This happens on install, browser startup, or after the stored selector metadata is older than six hours.

The extension did this

It downloads selector metadata and applies the returned DOM selectors to the TikTok workflow.

The selectors determine which profile, repost-tab, repost-list, and repost-button elements the content script searches for.

Captured request
GEThttps://api.cleartok.io/cdn/selectors

200 OK JSON containing "version":"1.0.6" and a top-level "selectors" object.

Selector metadata used by the TikTok workflow
  • Selector version
    1.0.6

    Lets the extension decide whether to replace the selectors already stored on your device.

  • Profile navigation selectors
    [data-e2e="nav-profile"], [data-e2e="repost-tab"], a[href*="repost"]

    Controls how the extension finds your TikTok profile and the Reposts tab before taking action.

  • Repost list selectors
    [data-e2e="user-repost-item-list"], [data-e2e="user-repost-item"]

    Controls which TikTok page elements the extension treats as reposted videos or photos.

  • Repost button selectors
    [data-e2e="video-share-repost"], button[aria-label*="repost" i]

    Controls which on-page button the extension looks for when removing a repost.

What's stored on your device

The selector set lives in extension storage and can change which TikTok page elements are clicked during the removal workflow.

Location
chrome.storage.local keys 'selectors' and 'selectorsMeta'
Contents
{  "selectors": {    "video": {      "repostItem": "[data-e2e=\"user-repost-item\"]",      "repostButton": [        "[data-e2e=\"video-share-repost\"]",        "[data-e2e=\"repost-action-tag\"]",        "button[aria-label*=\"repost\" i]",        "[class*=\"repost\"]"      ],      "repostListContainer": "[data-e2e=\"user-repost-item-list\"]"    },    "navigation": {      "repostTab": [        "[data-e2e=\"repost-tab\"]",        "[class*=\"PRepost\"]",        "a[href*=\"repost\"]",        "button[data-testid=\"repost-tab\"]"      ],      "navProfileLink": "[data-e2e=\"nav-profile\"]",      "editProfileButton": "[data-e2e=\"edit-profile-entrance\"]"    }  },  "selectorsMeta": {    "version": "1.0.6",    "fetchedAt": 1783840904922  }}
The code that does this

The fetch, storage write, and runtime reload path

Readable version

Background selector refresh

background.js
const META_URL = 'https://api.cleartok.io/cdn/selectors';const SELECTOR_CACHE_TTL = 1000 * 60 * 60 * 6; // 6 hoursasync function checkMetaAndUpdate(force = false) {  const { selectorsMeta } = await chrome.storage.local.get('selectorsMeta');  if (!force && selectorsMeta && Date.now() - selectorsMeta.fetchedAt < SELECTOR_CACHE_TTL) {    return;  }  try {    const res = await fetch(META_URL, { cache: 'no-cache' });    if (!res.ok) return;    const meta = await res.json();    if (!meta?.version || !meta?.selectors) {      console.warn('[ClearTok BG] Invalid meta file');      return;    }    if (selectorsMeta?.version === meta.version) {      console.log('[ClearTok BG] Selectors up-to-date');      return;    }    await chrome.storage.local.set({      selectors: meta.selectors,      selectorsMeta: { version: meta.version, fetchedAt: Date.now() }    });    console.log('[ClearTok BG] Selectors updated to version:', meta.version);    // 只通知正在处理的标签页(如果有)    if (stateManager.state.process.tabId) {      chrome.tabs.sendMessage(stateManager.state.process.tabId, {        type: 'SELECTORS_UPDATED',        timestamp: Date.now()      }).catch(() => { });    }  } catch (e) {    console.warn('[ClearTok BG] Failed to fetch selectors:', e);  }}chrome.runtime.onStartup?.addListener(() => checkMetaAndUpdate(true));chrome.runtime.onInstalled.addListener(() => {  console.log("[ClearTok BG] Extension installed");  checkMetaAndUpdate(true);  chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });});

Content-script selector loading

modules/config.js
async init() {  // 开发模式:跳过缓存,直接加载 fallback  if (!ConfigManager.FORCE_RELOAD_SELECTORS) {    try {      const { selectors } = await chrome.storage.local.get('selectors');      if (selectors && Object.keys(selectors).length > 0) {        const missing = ConfigManager._missingRequiredKeys(selectors);        if (missing.length === 0) {          this.selectors = selectors;          console.log('[ClearTok] Selectors loaded from storage.');          this.listenForUpdates();          return;        }        // 缓存陈旧:缺失新版必需 key —— 不使用,fall through 到 fallback        console.warn(          '[ClearTok] Cached selectors are stale (missing keys:',          missing.join(', '),          '). Reloading from bundled fallback JSON.'        );      }    } catch (error) {      console.error('[ClearTok] Error loading selectors from storage:', error);    }  } else {    console.log('[ClearTok] FORCE_RELOAD_SELECTORS is ON, loading from fallback...');  }  // 从后备文件加载  const fallbackUrl = chrome.runtime.getURL('assets/selectors-fallback.json');  try {    const res = await fetch(fallbackUrl);    const json = await res.json();    this.selectors = json.selectors;    console.log('[ClearTok] Selectors loaded from fallback JSON (version ' + (json.version || '?') + ').');    // 用 fallback 覆写缓存,避免下次又被陈旧缓存复活    try {      await chrome.storage.local.set({        selectors: json.selectors,        selectorsMeta: { version: json.version || 'fallback', fetchedAt: Date.now(), source: 'fallback' }      });    } catch (_) { /* 非关键 */ }  } catch (error) {    console.error('[ClearTok] Failed to load fallback selectors:', error);  }  this.listenForUpdates();}listenForUpdates() {  chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {    // 使用新的消息格式    if (msg.type === 'SELECTORS_UPDATED') {      chrome.storage.local.get('selectors').then(({ selectors }) => {        if (selectors) {          const missing = ConfigManager._missingRequiredKeys(selectors);          if (missing.length === 0) {            this.selectors = selectors;            console.log('[ClearTok] ✨ Selectors hot-reloaded!');          } else {            console.warn(              '[ClearTok] Ignored SELECTORS_UPDATED — incoming selectors missing required keys:',              missing.join(', ')            );          }        }        // 确认消息已处理        sendResponse({ success: true, result: 'reloaded selectors' });      });      // 保持通道开放以进行异步响应      return true;    }  });}
Remote service contacted for selector metadata
    • api.cleartok.io

    Receives the selector-metadata request and returns JSON that the extension stores for TikTok DOM element matching.

TikTok workflow events sent to ClearTok API

ClearTok sends event payloads to api.cleartok.io during repost removal, which can include your TikTok username, account ID, premium status, repost counts, totals, and page URL.

A PUT was observed; only a session-init payload was captured.

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

You start or use the TikTok repost-removal workflow.

Login checks, progress updates, workflow completion, errors, and element timeouts each call the event tracker.

The extension did this

The extension sends workflow event data to ClearTok's tracking endpoint.

The code builds JSON with event_name, session_id, version, run_id, and event-specific fields before making a PUT request.

Captured request
PUThttps://api.cleartok.io/session/track-event

Dynamic analysis observed a PUT to this endpoint with a session_initialized payload containing session_id, version, and run_id; username and process metrics were not captured in traffic.

Workflow event fields assembled by the extension
  • Session identifier
    sess_9f3d2a6c18b04e7a

    Lets ClearTok associate multiple workflow events from the same extension session.

  • TikTok username
    cleartok_user_247

    Identifies the TikTok account that the extension sees as logged in on the page.

  • Event name
    process_completed

    Shows which workflow stage happened, such as start, completion, timeout, or error.

  • TikTok page URL
    https://www.tiktok.com/@cleartok_user_247/repost

    Shows which TikTok page was open when an element lookup timed out.

  • Removal counts
    reposts_removed=24, processed_videos=31, skipped_videos=7

    Shows how many reposts were found, processed, removed, skipped, or failed during your run.

  • Timing and quota state
    total_duration_seconds=418, is_premium=false, remaining=76

    Shows how long the run lasted and what quota or subscription state was attached at the start.

  • Error and timeout details
    selector_key=video.repostButton, timeout_ms=15000, stop_reason=repost_tab_not_ready

    Shows which selector failed or why the workflow stopped when the extension encounters a problem.

The code that does this

The popup builds event payloads and the API service sends them

Readable version

Workflow start event

popup.js
async startRemoval() {  // 强制刷新配额  try { await this.quotaManager.getQuota(this.sessionAuth.sessionId, true); } catch (_) { }  const isPlus = !!this.quotaManager.quotaInfo.is_premium;  const remaining = Math.max(0, this.quotaManager.quotaInfo.remaining ?? this.quotaManager.getRemainingQuota());  if (!isPlus && remaining <= 0) {    const base = this.quotaManager.quotaInfo.authenticated      ? 'https://cleartok.io/premium/'      : 'https://cleartok.io/login/';    this._openUrlWithParams(base);    return;  }  // 上报 process_started  const uid = this.quotaManager.accountInfo?.id ?? null;  this.eventTracker.startRun({    is_premium: isPlus,    remaining_at_start: remaining,  });  this.eventTracker.track(EVENTS.PROCESS_START, {    uid,    is_premium: isPlus,    remaining,  });  await this.processLogger.startRemoval();}

Login, timeout, error, and completion events

popup.js
handleLoginStatusUpdate(message) {  if (message.isLoggedIn) {    if (message.username && message.username !== this.sessionAuth.tikTokUsername) {      this.sessionAuth.tikTokUsername = message.username;      this.sessionAuth.saveSessionToStorage();      this.eventTracker.track(EVENTS.USER_LOGIN, { tiktok_username: message.username });    }    this.sessionAuth.updateLoginStatus('loggedIn');  } else {    if (this.sessionAuth.tikTokUsername !== null) {      this.sessionAuth.tikTokUsername = null;      this.eventTracker.track(EVENTS.USER_LOGOUT);    }    this.sessionAuth.updateLoginStatus('notLoggedIn');    ClearTokUtils.showNotification(ClearTokUtils.getText('notificationPleaseLogin') || '⚠️ Please log in to TikTok.com to continue', 'error');  }}handleUiWaitTimeout(message) {  this.eventTracker.track(EVENTS.ELEMENT_TIMEOUT, {    selector_key: message.selectorKey,    timeout_ms: message.timeout,    page_url: message.url,  });}handleError(message) {  this.processLogger.handleError(message.message, message.error);  this.eventTracker.track(EVENTS.PROCESS_ERROR, {    error_message: message.message,    error_details: message.error ? String(message.error) : '',    status_category: message.statusCategory || 'error',    stop_reason: message.stopReason || 'unexpected_error',  });  if (message.message && message.message.includes('No active TikTok tab found')) {    this.processLogger.resetToInitialState();  }}handleComplete(message) {  // UI 更新(纯展示)  this.processLogger.handleCompletion(message);  // 数据上报(统一在此处编排)  const removedCount = message.removedCount || this.processLogger.removedVideos || 0;  const duration = message.duration || 0;  const totalDurationSeconds = this.sessionAuth.sessionStartTime    ? Math.floor((Date.now() - this.sessionAuth.sessionStartTime) / 1000)    : (duration ? Math.floor(duration.total / 1000) : 0);  const stopReason = message.stopReason || 'completed';  const statusCategory = ClearTokExtension.workflowStatusCategory(stopReason, removedCount);  this.eventTracker.track(EVENTS.PROCESS_COMPLETE, {    status_category: statusCategory,    stop_reason: stopReason,    total_reposts_found: message.totalCount || this.processLogger.totalVideos || 0,    reposts_removed: removedCount,    processed_videos: message.processedVideos || this.processLogger.processedVideos || 0,    skipped_videos: message.skippedVideos || 0,    failed_videos: message.failedVideos || 0,    batch_count: message.batchCount || 0,    skip_reasons: message.skipReasons || {},    total_duration_seconds: totalDurationSeconds,  });  if (removedCount > 0) {    this.eventTracker.reportUsage(removedCount);    this.quotaManager.updateUsage(removedCount);    this.sessionAuth.updateStartButtonLabel();  }}

Username extraction in the TikTok page

modules/workflow.js
async runInitialChecks() {    try {        const profileLink = this.ui.findElement('loginStatus.profileLink');        if (!profileLink || !profileLink.getAttribute('href')?.includes('/@')) {            await this.messageBus.broadcast('LOGIN_STATUS_UPDATE', { isLoggedIn: false });            return;        }        const href = profileLink.getAttribute('href');        const usernamePart = href.split('/@')[1];        const isLoggedIn = usernamePart && usernamePart.trim().length > 0;        const hasAvatar = this.ui.findElement('loginStatus.avatarImage', profileLink) !== null;        await this.messageBus.broadcast('LOGIN_STATUS_UPDATE', {            isLoggedIn: isLoggedIn,            username: isLoggedIn ? usernamePart.trim() : null,            hasUserAvatar: hasAvatar,        });        console.log('[ClearTok] Login status checked:', { isLoggedIn });    } catch (error) {        console.error('[ClearTok] Error checking login status:', error);        await this.messageBus.broadcast('LOGIN_STATUS_UPDATE', { isLoggedIn: false, error: error.message });    }}

Event tracker and PUT serialization

popup/modules/event-tracker.js and api-service.js
async track(eventName, data = {}) {  if (!this.sessionId) {    await ClearTokUtils.sleep(2000);    if (!this.sessionId) {      console.warn(`[EventTracker] 无 sessionId,跳过事件: ${eventName}`);      return;    }  }  if (ONCE_PER_PROCESS_EVENTS.has(eventName)) {    const key = `${this.sessionId}:${eventName}`;    if (this._sentEvents.has(key)) {      console.log(`[EventTracker] 去重跳过: ${eventName}`);      return;    }    this._sentEvents.add(key);  }  const payload = {    event_name: eventName,    run_id: this.runId,    ...this.runContext,    ...data,  };  try {    await this.api.updateSession(this.sessionId, payload);    console.log(`[EventTracker] ✅ ${eventName}`, data);  } catch (error) {    console.warn(`[EventTracker] 上报失败: ${eventName}`, error);  }}async updateSession(sessionId, data) {  if (!sessionId) {    console.warn('Session ID is empty, skipping event tracking.');    return { success: false, message: 'Session ID is required' };  }  // 确保 event_name 存在,因为这是新模型的强制要求  if (!data.hasOwnProperty('event_name')) {    console.error('API call is missing "event_name" property.', data);    return { success: false, message: 'event_name property is required for the new API model.' };  }  try {    const response = await this.request('/session/track-event', {      method: 'PUT',      body: JSON.stringify({        session_id: sessionId,        version: this.version,        ...data      })    });    return response;  } catch (error) {    console.error(`Failed to track event`, error);    // 不抛出错误,避免因分析数据上报失败而影响扩展的核心功能    return { success: false, error: error.message };  }}
Remote service receiving workflow events
    • api.cleartok.io

    Receives session and workflow tracking events for the ClearTok extension, including the /session/track-event endpoint.

Where it sends data

Destinations our analysis observed ClearTok 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.cleartok.io

    ClearTok sends data to api.cleartok.io. No other extension we have analysed sends data here.

Updated 30 September 2026kmellgkfemijicfcpndnndiebmkdginb