Is vidIQ Vision for YouTube safe?

Medium risk

vidIQ is medium risk. While signed in to vidIQ, browsing TikTok makes the extension record which videos you watched, how far, and rewatches, with the creator's handle, sound, and video URL, sent to vidIQ's analytics. Controlled by server-side experiment flags.…

vidIQv3.219.4Chrome Web Store
49Risk
Who publishes it

vidIQ - no other listings under this identity, 12 shared hostnames

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

Publisher
vidIQ

Shared hosts - 12 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.

assets.vidiq.com
Also called by 1 other listing: vidIQ Vision for YouTube
account.vidiq.com
Also called by 2 other listings, including vidIQ Vision for YouTube
dtmpce1fl6frx.cloudfront.net
Also called by 2 other listings
enterprise.vidiq.com
Also called by 2 other listings
local.vidiq.com
Also called by 2 other listings
mcp.vidiq.com
Also called by 2 other listings
static.vidiq.com
Also called by 2 other listings, including vidIQ Vision for YouTube
support.vidiq.com
Also called by 2 other listings
youtube-videos.vidiq.com
Also called by 2 other listings, including vidIQ Vision for YouTube
api.vidiq.com
Also called by 3 other listings
app.vidiq.com
Also called by 3 other listings
vidiq.com
Also called by 3 other listings, including Youtube Channels To CSV

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

Publishers can request a review.

Findings

TikTok Watch Behavior Sent to vidIQ Analytics

While signed in to vidIQ, browsing TikTok makes the extension record which videos you watched, how far, and rewatches, with the creator's handle, sound, and video URL, sent to vidIQ's analytics.

Controlled by server-side experiment flags.

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

You watch a TikTok video while signed in to vidIQ.

This applies to the for-you page, following feed, search results, hashtag pages, and individual video pages.

The extension did this

The extension sends your watch state, the video URL, creator handle, and sound metadata to a vidIQ analytics endpoint.

Three distinct events are reported: when playback starts, when the video completes, and if you rewatch it.

Captured request
POSThttps://cfriuxb2f3.execute-api.us-east-1.amazonaws.com/v1/ExtensionAnalytics

Amplitude analytics relay; 200 OK. Three separate requests captured during dynamic analysis: started_watching, completed_watching, rewatched.

Headers
Content-Type
application/json
Body
{  "events": [    {      "event_type": "started watching tiktok video",      "user_properties": {        "Fetch Override TikTok resalt2": "variant"      },      "event_properties": {        "video name": "POV you're listening to✨",        "video url": "https://www.tiktok.com/@sofiacamara/video/7613882455048604949",        "video creator": "sofiacamara",        "sound name": "original sound",        "sound author": "sofiacamara",        "sound url": "https://www.tiktok.com/music/original-sound-7613882455048604949"      }    }  ]}
Fields transmitted per watch event
  • Your vidIQ user ID
    8421937

    Links every TikTok video you watch to your vidIQ account.

  • Video URL
    https://www.tiktok.com/@sofiacamara/video/7613882455048604949

    The full URL of the TikTok video, including the creator's @handle and the numeric video ID.

  • Creator handle
    sofiacamara

    The TikTok username of the person who posted the video.

  • Watch state
    completed_watching

    Whether you started watching, watched to the end, or watched it a second time.

  • Timestamp
    1745547912384

    Millisecond-precision time when each watch event occurred.

  • Sound name and author
    original sound — sofiacamara

    The audio track used in the video and its creator's name.

The code that does this

fetch hook injection gated by TIKTOK_FETCH_OVERRIDE server flag (tiktokDocumentStart.bundle.js)

Readable version
// tiktokDocumentStart.bundle.js — document_start CS on www.tiktok.com/*// Queries the remote experiment flag; injects the fetch-override script only// when the server returns variant.key === 'variant'.experimentClient.getExperimentValue(FeatureFlags.TIKTOK_FETCH_OVERRIDE)  .then((result) => {    if (result && result.variant.key === 'variant') {      // inject tiktokFetchHandler.bundle.js (WAR) into the page's MAIN world      const script = document.createElement('script');      script.src = chrome.runtime.getURL('tiktokFetchHandler.bundle.js');      document.documentElement.appendChild(script);      script.parentNode?.removeChild(script);    }  });
The code that does this

window.fetch wrapper that intercepts TikTok API responses (tiktokFetchHandler.bundle.js)

Readable version
// tiktokFetchHandler.bundle.js — injected into MAIN world on www.tiktok.com/*// Wraps window.fetch to intercept TikTok's internal video-feed API responses.const INTERCEPTED_PATHS = [  '/api/recommend/item_list/',   // For You page  '/api/following/item_list/',   // Following feed  '/api/post/item_list/',        // User profile videos  '/api/challenge/item_list/',   // Hashtag page  '/api/music/item_list/',       // Music page  '/api/topic/item_list/',       // Topic feed  '/api/search/general/full/',   // Search results  '/api/related/item_list/',     // Related videos  'api/explore/item_list',       // Explore page];const PAGE_TYPE_MAP = {  recommend: 'foryou', following: 'following', post: 'channel',  challenge: 'hashtag', music: 'music', topic: 'foryou',  search: 'search', related: 'related', explore: 'explore',};const originalFetch = window.fetch;window.fetch = function (...args) {  return originalFetch(...args).then(async (response) => {    const matchedPath = INTERCEPTED_PATHS.find(p => response.url?.includes(p));    if (matchedPath) {      const segment = new URL(response.url).pathname.split('/')[2];      const pageType = PAGE_TYPE_MAP[segment];      if (pageType) {        const body = await response.clone().json().catch(() => response.text());        document.dispatchEvent(new CustomEvent('tiktokAjaxResponse', {          detail: { url: response.url, data: body, page: pageType },        }));      }    }    return response;  });};
Destinations for TikTok behavior data
    • cfriuxb2f3.execute-api.us-east-1.amazonaws.com

    AWS API Gateway relay to Amplitude. Receives watch-state events (started/completed/rewatched) with video URL, creator handle, sound metadata. Confirmed in dynamic analysis.

    • api.vidiq.com

    vidIQ's primary API. Receives batched video feed data (IDs, view/like/comment/share counts, author info, hashtags, duration) at api/scraping/tiktok/{apiPath} via POST.

    • api.vidiq.com

    Experiment flag source: api.vidiq.com/experimentation/batch-by-keys controls whether the fetch hook and watch-event collection are active for a given user.

ChatGPT Bearer Token Captured to Check vidIQ Connector

The extension installs a MAIN-world script on chatgpt.com replacing window.fetch to intercept the ChatGPT Bearer token, used in a same-origin POST to check if the vidIQ integration is active.

No transmission to vidIQ infra was observed.

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

You visit any page on chatgpt.com while vidIQ is installed.

This applies to every page navigation on chatgpt.com, including the login page, chat interface, and settings.

The extension did this

The extension overrides the browser's native fetch and XMLHttpRequest APIs to intercept your ChatGPT Bearer authentication token without a consent prompt.

The captured token is then used in a same-origin POST to the ChatGPT connector status endpoint to check if the vidIQ integration is active on your account.

The code that does this

window.fetch replacement and XHR header patch (chatgptAuthBridge.bundle.js)

Readable version
// Save the original native fetch before overriding itconst originalFetch = window.fetch;// Replace window.fetch to intercept authorization headers from every outgoing requestwindow.fetch = (input, init) => {  try {    // Extract Bearer token from the init.headers object (plain object, Headers instance, or [name,value][] array)    extractToken(init?.headers);    // Also check if input is a Request object with its own headers    if (input instanceof Request) extractToken(input.headers);  } catch (err) {    dispatchError(err, 'publish fetch authorization header');  }  // Call native fetch directly to avoid recursion  const promise = originalFetch.call(window, input, init);  // Sniff the connector status endpoint response when it matches  promise.then(resp => checkConnectionStatus(input, resp))         .catch(err => { if (isConnectorUrl(input)) dispatchError(err, 'read fetch connection status'); });  return promise;};// Patch XHR to capture Bearer tokens from setRequestHeader callsconst originalSetHeader = XMLHttpRequest.prototype.setRequestHeader;XMLHttpRequest.prototype.setRequestHeader = function(name, value) {  try {    if (name.toLowerCase() === 'authorization') storeBearerToken(value);  } catch (err) {    dispatchError(err, 'publish xhr authorization header');  }  return originalSetHeader.call(this, name, value);};
The code that does this

Connector status POST using the captured Bearer token

Readable version
// Calls the ChatGPT connector status endpoint with the intercepted Bearer tokenconst checkConnectorStatus = async () => {  // Return cached result if no token has been captured yet  if (!capturedBearerToken) {    if (connectorActive === true) return true;    if (connectorActive === false && Date.now() - lastChecked < 2000) return false;    return null;  }  const controller = new AbortController();  const timeout = window.setTimeout(() => controller.abort(), 8000);  try {    // Uses saved native fetch (b) to bypass the override; injects captured Bearer token    const response = await originalFetch.call(window, '/backend-api/aip/connectors/links/list_accessible', {      body: JSON.stringify({ link_refresh_strategy: 'BLOCKING', principals: [] }),      credentials: 'include',      headers: {        authorization: capturedBearerToken,  // <-- the intercepted ChatGPT Bearer token        'content-type': 'application/json'      },      method: 'POST',      signal: controller.signal    });    if (!response.ok) return null;    const data = await response.json();    // Active when connector_id matches + auth_status 'ACTIVE' + connector_status 'ENABLED'    return parseConnectorStatus(data);  } finally {    window.clearTimeout(timeout);  }};
Captured request
POSThttps://chatgpt.com/backend-api/aip/connectors/links/list_accessible

401 Unauthorized, 'Provided authentication token is expired'. Seven requests to this endpoint were captured during dynamic analysis, confirming the intercepted Bearer token was read from the page's own ChatGPT session requests and included in the Authorization header of each call.

Headers
content-type
application/json
authorization
Bearer <redacted>
Body
{  "link_refresh_strategy": "BLOCKING",  "principals": []}
Token stays on chatgpt.com

Dynamic analysis found no evidence that the intercepted Bearer token was forwarded to vidIQ servers. The token is used only for the same-origin connector status check on chatgpt.com. The extension's source code includes no user-facing disclosure of this API override on chatgpt.com.

Updated 30 September 2026pachckjkecffpdphbpmfolblodfkgbhl