Is Video Downloader Pro safe?
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.…
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
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.
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 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.
| Field | Value | Why 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. |
| Content-Type | application/json |
The click-to-POST path in the shipped extension
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"))}))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)}))})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()
}
}
})- vimego.io
Receives the selected Vimeo configuration object and returns media playlist URLs plus size information used by merge.html.
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.
You open any page on vimeo.com with a video player.
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.
The injection and hook, shipped source vs 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();// 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);
};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.
<body> <!-- normal Vimeo page content --> </body>
<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>
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.
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.
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 loads its Vimeo content script and stylesheet into the page.
The manifest also enables the rule in every frame on the page.
| Field | Value | Why 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 DOM | The script can inspect the page structure and visible text that your browser has loaded. | |
Every embedded frame | all_frames: true | The rule also applies inside frames, which broadens the pages and widgets the extension code can touch. |
The manifest scopes Vimeo code to all sites, and the script touches the page DOM
content_scripts: [{
matches: ["<all_urls>"],
js: ["js/content-script-vimeo.js"],
css: ["css/insert-vimeo.css"],
all_frames: true,
run_at: "document_end"
}]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 };
}