Is Video to MP3 safe?

Medium risk

Video to MP3 sends a built-in API token with format and download requests to yt-gateway.vercel.app.

When you use the extension to convert or download media, its scripts contact yt-gateway.vercel.app for formats, downloads, progress, files, password status, and delivery statistics. Those requests include a hardcoded X-API-Token header embedded in the extension code.

45Risk

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

Publishers can request a review.

Findings

Hardcoded API token sent to conversion gateway

The extension embeds a gateway API token used in conversion requests.

Opening the popup on a YouTube video or continuing a download sends your video URL, a persistent install ID, and the bundled X-API-Token header to yt-gateway.vercel.app.

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

You choose a video and ask the extension to fetch formats or start a download.

The extension did this

The extension sends requests to its conversion gateway with a bundled API token in the headers.

Fields attached to gateway requests
  • Gateway API token
    X-API-Token: <redacted>

    A service credential is shipped inside every installed copy and is sent back to the conversion service.

  • Selected video URL
    https://www.youtube.com/watch?v=dQw4w9WgXcQ

    The conversion gateway receives the video link you chose for format lookup or download processing.

  • Installation ID
    X-User-ID: 4f5b7c0a-3a4d-4a9b-9a14-6e928c7e4b21

    A persistent browser-generated ID lets the service associate repeated conversion requests from the same installation.

  • Extension brand
    X-Brand: youtube-to-mp3

    The gateway can tell which extension product sent the request.

Captured request
POSThttps://yt-gateway.vercel.app/formats
Headers
X-API-Token
<redacted>
Content-Type
application/json
Captured request
POSThttps://yt-gateway.vercel.app/download
Headers
X-API-Token
<redacted>
Content-Type
application/json
The code that does this

The shipped scripts embed the token and reuse it in gateway headers

Readable version

popup.js defaults and format request

popup.js
const defaults = {  serverUrl: "https://yt-gateway.vercel.app",  apiToken: "<redacted>"};let gatewayUrl = defaults.serverUrl;let apiToken = defaults.apiToken;let userId = "";chrome.storage.sync.get({  ...defaults,  userId: "",  audioFormat: "mp3",  audioQuality: "320"}, settings => {  gatewayUrl = settings.serverUrl;  apiToken = settings.apiToken;  if (settings.userId) {    userId = settings.userId;  } else {    userId = crypto.randomUUID();    chrome.storage.sync.set({ userId });  }});async function fetchFormats() {  if (!selectedVideoUrl) return;  let formats = readFormatCache(selectedVideoUrl);  if (!formats) {    const response = await fetch(`${gatewayUrl}/formats`, {      method: "POST",      headers: gatewayHeaders(),      body: JSON.stringify({ url: selectedVideoUrl })    });    if (response.status === 403) throw new Error(t("invalidToken"));    if (response.status === 429) throw new Error(t("serverBusy"));    if (!response.ok) {      const error = await response.json();      throw new Error(error.detail || t("fetchFailed"));    }    formats = await response.json();    writeFormatCache(selectedVideoUrl, formats);  }  renderFormats(formats);}function gatewayHeaders() {  return {    "Content-Type": "application/json",    "X-API-Token": apiToken,    "X-User-ID": userId,    "X-Brand": "youtube-to-mp3"  };}

download.js download, progress, file, status, and stats requests

download.js
const defaults = {  serverUrl: "https://yt-gateway.vercel.app",  apiToken: "<redacted>"};let gatewayUrl = defaults.serverUrl;let apiToken = defaults.apiToken;let userId = "";async function startDownload() {  const payload = {    url: videoUrl,    format_id: formatId,    ext: extension,    audio_quality: audioQuality  };  if (startTime != null) payload.start = parseFloat(startTime);  if (endTime != null) payload.end = parseFloat(endTime);  const response = await fetch(`${gatewayUrl}/download`, {    method: "POST",    headers: gatewayHeaders(),    body: JSON.stringify(payload)  });  if (!response.ok) throw new Error(t("serverError"));  const task = await response.json();  const workerUrl = task.worker_url || gatewayUrl;  pollProgress(task.task_id, workerUrl);}async function pollProgress(taskId, workerUrl) {  const response = await fetch(`${workerUrl}/progress/${taskId}`, {    headers: {      "X-API-Token": apiToken,      "X-User-ID": userId    }  });  const progress = await response.json();  if (progress.status === "done") {    chrome.downloads.download({      url: `${workerUrl}/file/${taskId}`,      headers: [        { name: "X-API-Token", value: apiToken },        { name: "X-User-ID", value: userId },        { name: "X-Brand", value: "youtube-to-mp3" }      ]    });  }}function gatewayHeaders() {  return {    "Content-Type": "application/json",    "X-API-Token": apiToken,    "X-User-ID": userId,    "X-Brand": "youtube-to-mp3"  };}function sendPaywallStatus(status) {  fetch(`${gatewayUrl}/pw-status`, {    method: "POST",    headers: gatewayHeaders(),    body: JSON.stringify(status)  }).catch(() => {});}function sendDeliveryStats(workerUrl, taskId, bytes, seconds) {  fetch(`${workerUrl}/internal/delivery-stats`, {    method: "POST",    headers: gatewayHeaders(),    body: JSON.stringify({ task_id: taskId, bytes, seconds })  }).catch(() => {});}

options.js writes the same default token to settings

options.js
const defaults = {  serverUrl: "https://yt-gateway.vercel.app",  apiToken: "<redacted>"};ready(() => {  localizeSettingsPage();  populateLanguageSelect();  chrome.storage.sync.get(defaults, settings => {    serverUrlInput.value = settings.serverUrl;    apiTokenInput.value = settings.apiToken;  });});saveButton.addEventListener("click", () => {  const serverUrl = serverUrlInput.value.trim().replace(/\/+$/, "");  const apiToken = apiTokenInput.value.trim();  const language = languageSelect.value;  saveLanguage(language);  chrome.storage.sync.set({ serverUrl, apiToken }, () => {    savedNotice.classList.add("visible");    setTimeout(() => {      savedNotice.classList.remove("visible");      if (language !== currentLanguage()) location.reload();    }, 1500);  });});
Remote services named in the request flow
    • yt-gateway.vercel.app

    Receives the format, download, progress, file, paywall-status, and delivery-stat requests for the conversion workflow.

    • app.yt-video-to-mp3.club

    Configured as the paywall SDK origin in the same extension bundle; not the endpoint carrying the X-API-Token header.

Where it sends data

Destinations our analysis observed Video to MP3 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.

  • yt-gateway.vercel.app

    Video to MP3 sends data to yt-gateway.vercel.app. No other extension we have analysed sends data here.

Updated 30 September 2026ogoimgmekoacpndfanncfplddpjnecjm