Is Video Downloader Pro safe?

Medium risk

Video Downloader Pro is medium risk. Clicking the Vimeo download button, the extension posts the video's HLS config URL, cover image, title, duration, and quality to vimego.io before downloading segments. Evidence covers the request URL, method, and headers, not the body.…

save.highvideov1.1.14Chrome Web Store
45Risk

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

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

Vimeo download config is posted to vimego.io

Clicking the Vimeo download button, the extension posts the video's HLS config URL, cover image, title, duration, and quality to vimego.io before downloading segments.

Evidence covers the request URL, method, and headers, not the body.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You click a Vimeo download option inserted by the extension.

The button corresponds to a specific quality choice for the video on the page.

The extension did this

The extension sends that video's configuration data to vimego.io before completing the download.

The object includes the HLS configuration URL, cover image URL, title, duration, and quality identifier.

02EvidenceFIELD TABLE
Fields assembled into config_body before the POST
FieldValueWhy it matters
Vimeo HLS configuration URL
https://vod-adaptive-ak.vimeocdn.com/exp=1720915200/abcdef1234567890/master.json?base64_init=1 (illustrative)Shows which Vimeo media configuration you asked the extension to process.
Cover image URL
https://i.vimeocdn.com/video/123456789-640x360.jpg (illustrative)Identifies the video artwork associated with your download request.
Video title
Staff Pick: Mountain Light (illustrative)Reveals the title of the Vimeo video you selected.
Duration
184.52 seconds (illustrative)Adds playback-length context to the video selection.
Quality identifier
1080p (illustrative)Shows which rendition of the video you selected for download.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://vimego.io/ffmpeg/vimeo-config/
The code expects JSON containing video_url, audio_url, and size for the requested download.
Headers
Content-Typeapplication/json
04EvidenceCODE COMPARE
The code that does this

The click-to-POST path in the shipped extension

What it actually does
Deobfuscated bundle click handlerjs/content-script-vimeo.js
i()("body").on("click"," .vt-web-download",(async function(){let t={private:1,qid:i()(this).attr("qid"),hlsurl:i()(this).attr("hlsurl"),cover:i()(this).attr("cover"),title:i()(this).attr("title"),duration:i()(this).attr("duration")};i()(this).addClass("attention");try{chrome.runtime.sendMessage({action:"vimeo-download-merge",config_body:t}),i()(this).removeClass("attention")}catch(e){console.error("Error fetching video configuration:",e)}Object(s["b"])("Download-pro",this.innerText,i()(this).attr("hlsurl"))}))
Deobfuscated bundle relay handlerjs/background.js
chrome.runtime.onMessage.addListener(async(t,e,r)=>{"vimeo-download-merge"===t.action&&chrome.tabs.create({url:"merge.html"},(function(e){setTimeout(()=>{chrome.tabs.sendMessage(e.id,{action:"merge-video",config_body:t.config_body})},1e3)}))})
Readable merge handlerjs/merge.js
chrome.runtime.onMessage.addListener(async (msg, sender, sendResponse) => {
    if (msg.action === "merge-video") {
        try {
            let response = await fetch("https://vimego.io/ffmpeg/vimeo-config/", {
                method: "POST",
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(msg.config_body)
            });

            let res_json = JSON.parse(await response.json());
            let video_url = res_json.video_url;
            let audio_url = res_json.audio_url;
            let video_size = res_json.size;

            let resp = await fetch(video_url);
            let text = await resp.text();
            let videoUrls = parse_m3u8(video_url, text);

            let audioUrls;
            if (audio_url) {
                let resp = await fetch(audio_url);
                let text = await resp.text();
                audioUrls = parse_m3u8(audio_url, text);
            } else {
                audioUrls = [];
            }
            const fileList = gen_file_list(videoUrls.length, video_size)

            let totalParts = videoUrls.length + audioUrls.length
            document.getElementById("waiting").style.display = "none"
            document.getElementById("down_show").style.display = "flex"
            document.getElementById("video_title").innerText = sanitize(msg.config_body.title)
            document.getElementById("video_cover").src = msg.config_body.cover

            let processedParts = 0
            let data

            const onFetched = () => {
                processedParts++;
                updateProgress((processedParts / totalParts * 99).toFixed(1))
            };

            // video启动片段
            let videoInitBuffer = await fetchBuffer(videoUrls[0], { maxRetries: 3 })
            if (!ffmpeg.isLoaded()) {
                await ffmpeg.load();
            }
            for (let [index, fileRange] of Object.entries(fileList)) {
                if (!ffmpeg.isLoaded()) {
                    await ffmpeg.load();
                }
                const videoBuffers = [videoInitBuffer, ...await fetchBuffers(videoUrls.slice(fileRange[0], fileRange[1]), onFetched)]
                await writeBuffersToFile(videoBuffers, 'video_merge.mp4');
                if (audioUrls.length === 0) {
                    data = ffmpeg.FS('readFile', 'video_merge.mp4');
                } else {
                    // audio启动片段
                    let audioInitBuffer = await fetchBuffer(audioUrls[0], { maxRetries: 3 })
                    const audioBuffers = [audioInitBuffer, ...await fetchBuffers(audioUrls.slice(fileRange[0], fileRange[1]), onFetched)]
                    await writeBuffersToFile(audioBuffers, 'audio_merge.mp4');
                    await ffmpeg.run('-i', 'video_merge.mp4', '-i', 'audio_merge.mp4', '-c:v', 'copy', '-c:a', 'aac', '-strict', 'experimental', 'merge.mp4');
                    data = ffmpeg.FS('readFile', 'merge.mp4');
                }
                // 下载
                var fileBlob = new Blob([data.buffer], { type: "application/octet-stream;" });
                let link = document.createElement('a');
                link.href = window.URL.createObjectURL(fileBlob);
                link.download = sanitize(msg.config_body.title, "") + '.mp4';
                link.click()
                updateProgress(100)
                clearFFmpegFiles()
                await ffmpeg.exit();
            }
        } catch (e) {
            await ffmpeg.exit()
        }
    }
})
05EvidenceTHIRD PARTY LIST
Remote host in this download path
  • vimego.io

    Receives the selected Vimeo configuration object and returns media playlist URLs plus size information used by merge.html.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

XHR Prototype Hook Intercepts Vimeo API Responses

On any Vimeo page, the extension wraps the page's XMLHttpRequest, exposing every Vimeo API response including authenticated CDN URLs with signed tokens.

Dynamic analysis confirmed it intercepted these to fetch video segments.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You open any page on vimeo.com with a video player.

The extension did this

The extension injects a script into the page that wraps XMLHttpRequest, giving itself access to every API response the Vimeo player sends and receives.

The hook runs at page-context level, bypassing Chrome's content script isolation, meaning it can read response data that content scripts cannot.

02EvidenceCODE COMPARE
The code that does this

The injection and hook, shipped source vs readable form

What it actually does
content-inject.js — readable form
// Runs at document_start on all *.vimeo.com pages.
// Injects the ajax-listener script into the PAGE context (not content script context),
// so it can access page-level XHR objects and response data.
function injectAjaxListener() {
  const script = document.createElement('script');
  script.src = chrome.runtime.getURL('js/ajax-listener.js');
  (document.body || document.head || document.documentElement).appendChild(script);
}
injectAjaxListener();
ajax-listener.js — readable form
// Overwrites the global XHR open() method so every new XHR gets a 'load' listener.
// The original open() is saved as _origOpen and called at the end.
if (typeof XMLHttpRequest.prototype._origOpen === 'undefined') {
  XMLHttpRequest.prototype._origOpen = XMLHttpRequest.prototype.open;
}
XMLHttpRequest.prototype.open = function (...args) {
  this.addEventListener('load', function (event) {
    try {
      const responseText = JSON.parse(this.responseText);

      // Check if this response looks like a Vimeo CDN config
      const isVimeoConfig =
        responseText.request &&
        responseText.request.files &&
        responseText.cdn_url &&
        responseText.cdn_url.includes('vimeo') &&
        !document.querySelector('.variant-v2');

      if (isVimeoConfig) {
        const params = parseUrlParams(this.responseURL);
        // Only act when the response URL contains specific signals
        const isTarget =
          params.referrer ||
          this.responseURL.includes('ask_ai') ||
          this.responseURL.includes('access_gates') ||
          vimeoReviewRegex.test(window.location.href);

        if (isTarget) {
          const configDiv = document.querySelector('.vtConfigUrl');
          if (!configDiv) {
            // First intercept: write URL to a hidden DOM element
            document.body.insertAdjacentHTML(
              'beforeend',
              `<div class="vtConfigUrl" url=${event.currentTarget.responseURL}></div>`
            );
          } else {
            // Subsequent intercepts: update the element and fire postMessage
            configDiv.setAttribute('url', event.currentTarget.responseURL);
            window.postMessage({ type: 'configUrl', url: event.currentTarget.responseURL });
          }
        }
      }
    } catch (e) { /* silently ignore parse errors */ }
  });
  // Call the original open() so the XHR still works normally
  XMLHttpRequest.prototype._origOpen.apply(this, args);
};
03EvidenceDOM DIFF
Page DOM modified

Target: document.body on a Vimeo video page after the player API responds

A not user-visible <div class='vtConfigUrl'> element is inserted at the end of the body, its url attribute set to the authenticated Vimeo CDN configuration URL extracted from the intercepted XHR response.

Before
<body>
  <!-- normal Vimeo page content -->
</body>
After (modified by extension)
<body>
  <!-- normal Vimeo page content -->
  <div class="vtConfigUrl" url=https://player.vimeo.com/video/76979871/config?autopause=1&byline=1&collections=1&context=Vimeo%5CController%5CClipController.main&default_to_hd=1&outro=nothing&portrait=1&referrer=https%3A%2F%2Fvimeo.com%2F76979871&title=1&watch_full_video=1></div>
</body>
04EvidencePLAIN NOTE
Why intercepting this URL matters

Vimeo's CDN configuration endpoint returns signed tokens (HMAC ACL parameters in the URL) that authorize access to the actual video stream segments. Normally these tokens are used only by the official Vimeo player. By intercepting and storing the configuration URL, the extension gives itself — and any other script that can read the DOM — the credentials to fetch video content directly from Vimeo's CDN without going through Vimeo's normal download controls. Our dynamic analysis observed the extension subsequently fetching 25 authenticated video segments from vod-adaptive-ak.vimeocdn.com using tokens obtained via this mechanism.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

Vimeo content script runs on every website

We observed Video Downloader Pro's Vimeo content script loading on www.google.com, a non-Vimeo page.

The manifest matches it against every URL and frame, so unrelated pages get extension code able to inspect and modify the DOM.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You navigate to a website with the extension installed.

The rule is not limited to Vimeo, so unrelated sites can trigger it too.

The extension did this

The extension loads its Vimeo content script and stylesheet into the page.

The manifest also enables the rule in every frame on the page.

02EvidenceFIELD TABLE
What becomes reachable to the injected script
FieldValueWhy it matters
The page you opened
https://www.google.com/This ties the extension's page access to the site you are currently using, even when the site is unrelated to Vimeo.
Visible page content
Google search page DOMThe script can inspect the page structure and visible text that your browser has loaded.
Every embedded frame
all_frames: trueThe rule also applies inside frames, which broadens the pages and widgets the extension code can touch.
03EvidenceNETWORK CAPTURE
Captured request
GETchrome-extension://penndbmahnpapepljikkjmakcobdahne/css/price.css
The stylesheet load was observed at 23:51:01.979Z, 624 ms after www.google.com first loaded at 23:51:01.355Z; three total loads occurred on non-Vimeo pages in the run.
04EvidenceCODE COMPARE
The code that does this

The manifest scopes Vimeo code to all sites, and the script touches the page DOM

What it actually does
Readable manifest scopemanifest.json
content_scripts: [{
  matches: ["<all_urls>"],
  js: ["js/content-script-vimeo.js"],
  css: ["css/insert-vimeo.css"],
  all_frames: true,
  run_at: "document_end"
}]
Readable DOM insertion functionjs/content-script-vimeo.js
async function injectPriceWidget() {
  const host = document.createElement("div");
  host.id = "vimeo_tool";
  host.className = "vimeo_tool";
  $("body").append(host);

  const shadowRoot = host.attachShadow({ mode: "open" });
  const style = document.createElement("style");
  style.textContent = await (await fetch(chrome.runtime.getURL("css/price.css"))).text();
  shadowRoot.appendChild(style);

  const mount = document.createElement("div");
  shadowRoot.appendChild(mount);

  const priceVm = new Vue({ el: mount, render: render => render(PriceComponent) });
  const changePriceSwitch = (visible = true) => {
    priceVm.$children[0].showPrice = visible;
  };

  return { priceVm, changePriceSwitch };
}
Updated 30 September 2026penndbmahnpapepljikkjmakcobdahne