Is TubeBuddy for YouTube™ safe?
TubeBuddy is medium risk. Analysis shows TubeBuddy injects a bridge on www.tubebuddy.com reading Chrome sync storage and dispatching a snapshot to the page. Channel tokens live under tubebuddyToken-{channelId}; unauth tests lacked them, confirming only the bridge.
Who publishes itBEN Group, Inc. - no other listings under this identity, 4 shared hostnames
BEN Group, Inc. - no other listings under this identity, 4 shared hostnames
What this publisher told the store about itself, and the other listings that told it the same thing.
Shared hosts - 4 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
TubeBuddy storage bridge exposes YouTube tokens to its web app
Analysis shows TubeBuddy injects a bridge on www.tubebuddy.com reading Chrome sync storage and dispatching a snapshot to the page.
Channel tokens live under tubebuddyToken-{channelId}; unauth tests lacked them, confirming only the bridge.
You use TubeBuddy's web application with the browser extension installed.
The extension is configured to run its bridge on www.tubebuddy.com and on YouTube pages.
The extension lets the TubeBuddy page request selected extension storage values.
If token keys are present, the same storage area holds per-channel TubeBuddy token values.
| Field | Value | Why it matters | |
|---|---|---|---|
YouTube channel ID | UCAbCdEfGhIjKlMnOpQrStUV (illustrative) | This ties the stored token to a specific YouTube channel you manage. | |
Chrome storage key | tubebuddyToken-UCAbCdEfGhIjKlMnOpQrStUV (illustrative) | This is where the extension saves the channel token before the TubeBuddy page can request extension state. | |
Channel token | 4/0AbUR2VN8qYxL9mQp7R3sT6uVwZaBcDeFgHiJkLmNoPqRsTuVwXyZ (illustrative) | This value can authorize TubeBuddy-related operations for the associated YouTube channel. | |
Bridge request ID | labs-request-1720641695123 (illustrative) | This lets the page match a storage snapshot response to the request it sent. |
The test browser did not have the signed-in YouTube channel state needed to produce a real token value during the observed run.
chrome.storage.sync during dynamic analysis{}The shipped bridge reads extension storage and returns it to the page
const TOKEN_SYNC_EVENT = 'tubebuddySyncTokens';
const TOKEN_SYNC_SOURCE = 'tbLabs';
const TOKEN_STORAGE_PREFIX = 'tubebuddyToken-';
const LABS_STATE_REQUEST_EVENT = 'tbLabsStateRequest';
const LABS_STATE_RESPONSE_EVENT = 'tbLabsStateResponse';
const LABS_STATE_KEYS = ['appToExtData', 'LifeCycleDeepLinkData'];
const STUDIO_HOSTNAME = 'studio.youtube.com';
const STUDIO_CHANNEL_STORAGE_KEY = 'activeYouTubeStudioChannel';
function getTokenSyncStorage() {
if (typeof browser !== 'undefined' && browser.storage && browser.storage.sync) {
return {
set: (items) => browser.storage.sync.set(items),
remove: (keys) => browser.storage.sync.remove(keys),
get: (keys) => browser.storage.sync.get(keys || null),
};
}
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.sync) {
return {
set: (items) => new Promise((resolve, reject) => {
chrome.storage.sync.set(items, () => {
const err = chrome.runtime && chrome.runtime.lastError;
if (err) reject(new Error(err.message));
else resolve();
});
}),
remove: (keys) => new Promise((resolve, reject) => {
chrome.storage.sync.remove(keys, () => {
const err = chrome.runtime && chrome.runtime.lastError;
if (err) reject(new Error(err.message));
else resolve();
});
}),
get: (keys) => new Promise((resolve, reject) => {
chrome.storage.sync.get(keys || null, (items) => {
const err = chrome.runtime && chrome.runtime.lastError;
if (err) reject(new Error(err.message));
else resolve(items || {});
});
}),
};
}
return null;
}
function isTrustedTokenSyncHost() {
const host = window.location.hostname || '';
return host === 'localhost' || host.endsWith('tubebuddy.com') || host.includes('studio.youtube.com');
}window.addEventListener(LABS_STATE_REQUEST_EVENT, (event) => {
try {
if (!isTrustedTokenSyncHost()) return;
const detail = event && event.detail;
if (!detail || detail.source !== TOKEN_SYNC_SOURCE) return;
sendLabsStateSnapshot(detail.reason || 'labs-request', detail.requestId, detail.keys, true);
} catch (error) {
window.dispatchEvent(new CustomEvent(LABS_STATE_RESPONSE_EVENT, {
detail: {
source: TOKEN_SYNC_SOURCE,
requestId: event && event.detail ? event.detail.requestId : undefined,
success: false,
error: error && error.message ? error.message : 'Unexpected error retrieving extension state',
studioChannelId: undefined,
channelId: undefined,
},
}));
}
}, false);
function sendLabsStateSnapshot(reason, requestId, keys, allowNonStudioHost = false) {
try {
if (!isTrustedTokenSyncHost()) return;
if (!allowNonStudioHost && !isStudioHost()) return;
const storage = getTokenSyncStorage();
if (!storage || typeof storage.get !== 'function') return;
const keysToFetch = Array.isArray(keys) && keys.length > 0 ? keys : LABS_STATE_KEYS;
const snapshotRequestId = requestId || `${reason || 'labs'}-${Date.now()}`;
const channelKeys = [STUDIO_CHANNEL_STORAGE_KEY];
storage.get(keysToFetch.concat(channelKeys))
.then((snapshot) => {
const channelFromStorage = snapshot ? snapshot[STUDIO_CHANNEL_STORAGE_KEY] : undefined;
const studioChannelId = typeof channelFromStorage === 'string' ? channelFromStorage.trim() : '';
if (snapshot && STUDIO_CHANNEL_STORAGE_KEY in snapshot) {
delete snapshot[STUDIO_CHANNEL_STORAGE_KEY];
}
const normalizedStudioChannelId = studioChannelId.length > 0 ? studioChannelId : undefined;
window.dispatchEvent(new CustomEvent(LABS_STATE_RESPONSE_EVENT, {
detail: {
source: TOKEN_SYNC_SOURCE,
requestId: snapshotRequestId,
success: true,
data: snapshot || {},
reason,
channelId: normalizedStudioChannelId,
studioChannelId: normalizedStudioChannelId,
keys: keysToFetch,
},
}));
})
.catch((error) => {
window.dispatchEvent(new CustomEvent(LABS_STATE_RESPONSE_EVENT, {
detail: {
source: TOKEN_SYNC_SOURCE,
requestId: snapshotRequestId,
success: false,
error: error && error.message ? error.message : 'Failed to read extension storage',
reason,
channelId: undefined,
studioChannelId: undefined,
keys: keysToFetch,
},
}));
});
} catch (error) {
}
}var tokenParam = getUrlParameter('t');
var channelParam = getUrlParameter('c');
var redirectParam = getUrlParameter('r');
if (tokenParam) {
if (tokenParam == 'remove') {
// When debugging with web-ext change sync to local in line below.
chrome.storage.sync.remove("tubebuddyToken-" + channelParam, function () { console.log('token removed'); })
}
else {
var dbSaveObject = {};
dbSaveObject["tubebuddyToken-" + channelParam] = tokenParam;
// When debugging with web-ext change sync to local in line below.
chrome.storage.sync.set(dbSaveObject, function () {
if (redirectParam) {
var url = '';
if (window.location.href.indexOf('studio.youtube.com') >= 0) {
redirectParam = decodeURIComponent(redirectParam);
var poundLocation = redirectParam.indexOf('#');
var fullPath = redirectParam.substring(poundLocation);
if (redirectParam.indexOf('cid=') > 0) {
url = redirectParam.substring(0, poundLocation) + '&tbft=1' + fullPath;
} else {
url = 'https://studio.youtube.com/?tbft=1' + fullPath;
}
} else {
if (redirectParam.indexOf('?') > 0 || redirectParam.indexOf('%3F') > 0 || redirectParam.indexOf('%3f') > 0)
url = decodeURIComponent(redirectParam) + '&tbft=1';
else
url = decodeURIComponent(redirectParam) + '?tbft=1';
}
window.location = url;
}
});
}
}The manifest places the bridge on TubeBuddy and YouTube pages
{
"matches": [
"https://www.tubebuddy.com/*",
"https://*.youtube.com/*",
"http://*.youtube.com/*"
],
"js": [
"js/webextension-polyfill/browser-polyfill.js",
"TubeBuddyAppToExt.js"
],
"run_at": "document_start",
"all_frames": true
}- www.tubebuddy.com
TubeBuddy's web application receives the DOM event response from the extension bridge when it requests extension state.
What it can do
Permissions this extension asks for, as declared in version 2002. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to. The current listed version is 2162, which we have not unpacked yet.
Read and change your data on www.tubebuddy.com
https://www.tubebuddy.com/*
Read and change your data on youtube.com
https://*.youtube.com/*
Read and change your data on google.com
https://*.google.com/*
Read and change your data on twitter.com
https://*.twitter.com/*
Read and change your data on facebook.com
https://*.facebook.com/*
Read and change your data on reddit.com
https://*.reddit.com/*
Store data in your browser
storage