Is TubeBuddy for YouTube™ safe?

Medium risk

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.

TubeBuddyv2162Chrome Web Store
45Risk
Who publishes it

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.

Publisher
TubeBuddy
Declared legal entity
BEN Group, Inc.
Registered address
14724 Ventura Blvd # 1200, Sherman Oaks, CA 91403-3512, US
Registered contact
Tyler Folkman

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.

9to5google.com
Also called by 5 other listings, including Black Menu Google
justgage.com
Also called by 5 other listings, including ORGanizer for Salesforce
strangeplanet.fr
Also called by 5 other listings, including وسهام | VaSaham
longdomainname.com
Also called by 6 other listings, including Old Twitter Layout, RiteTag, Old Twitter Layout (2026)

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

Publishers can request a review.

Findings

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
What actually happens
You did this

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 did this

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.

02EvidenceFIELD TABLE
Fields the bridge and token writer use
FieldValueWhy 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.
03EvidenceNETWORK CAPTURE
Captured request
GEThttps://www.tubebuddy.com/signin
Observed during dynamic analysis after navigation to www.tubebuddy.com; no token-bearing POST was captured because the test profile was not signed in.
04EvidenceSTORAGE DUMP
What's stored on your device

The test browser did not have the signed-in YouTube channel state needed to produce a real token value during the observed run.

Locationchrome.storage.sync during dynamic analysis
Contents (JSON)
{}
05EvidenceCODE COMPARE
The code that does this

The shipped bridge reads extension storage and returns it to the page

What it actually does
Readable summary of the storage bridgeTubeBuddyAppToExt.js
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');
}
Readable summary of the page-request handlerTubeBuddyAppToExt.js
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) {
  }
}
Readable summary of token storageTubeBuddyTokenWriter.js
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;
            }

        });
    }

}
06EvidenceCODE COMPARE
The code that does this

The manifest places the bridge on TubeBuddy and YouTube pages

What it actually does
{
  "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
}
07EvidenceTHIRD PARTY LIST
Destination involved in the bridge
  • 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

Updated 30 September 2026mhkhmbddkmdggbhaaaodilponhnccicb