Is vidIQ Vision for YouTube safe?
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.…
Who publishes itvidIQ - no other listings under this identity, 12 shared hostnames
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.
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.
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.
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 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.
| Content-Type | application/json |
{
"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"
}
}
]
}| Field | Value | Why it matters | |
|---|---|---|---|
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. |
fetch hook injection gated by TIKTOK_FETCH_OVERRIDE server flag (tiktokDocumentStart.bundle.js)
// 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);
}
});window.fetch wrapper that intercepts TikTok API responses (tiktokFetchHandler.bundle.js)
// 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;
});
};- 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.
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 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.
window.fetch replacement and XHR header patch (chatgptAuthBridge.bundle.js)
// Save the original native fetch before overriding it
const originalFetch = window.fetch;
// Replace window.fetch to intercept authorization headers from every outgoing request
window.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 calls
const 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);
};Connector status POST using the captured Bearer token
// Calls the ChatGPT connector status endpoint with the intercepted Bearer token
const 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);
}
};| content-type | application/json |
| authorization | Bearer <redacted> |
{
"link_refresh_strategy": "BLOCKING",
"principals": []
}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.