Is Video Downloader for Chrome - M3U8 Video Downloader | 4saved safe?

High risk

Video Downloader for Chrome - M3U8 Video Downloader | 4saved is high risk. Our dynamic analysis captured a startup GET to 4saved.com/domains.json?v=1, applied via updateDynamicRules.…

4savedv1.0.14Chrome Web Store
77Risk

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

Publishers can request a review.

Findings

Remote-injected declarativeNetRequest rules strip CSP and forge CORS headers

Our dynamic analysis captured a startup GET to 4saved.com/domains.json?v=1, applied via updateDynamicRules.

Rules strip CSP/X-Frame-Options, set Access-Control-Allow-Origin to *, and spoof Referer/Origin for CDNs, on every site.

Severity
High unwanted
Type
Unexpected
CWE
CWE-829
Source
Dynamic sandbox
What actually happens
You did this

You install the extension and open any browser session.

The service worker runs at startup and checks whether the remote config was fetched in the past 24 hours.

The extension did this

The extension fetches a rule list from 4saved.com and applies it to every website you visit.

chrome.declarativeNetRequest.updateDynamicRules replaces all existing dynamic rules with the operator-supplied set, which modifies response headers globally.

Captured request
GEThttps://4saved.com/domains.json?v=1

Returns a JSON array of declarativeNetRequest rule objects. Observed rules include: removeHeaders actions for content-security-policy and x-frame-options; setHeaders action for access-control-allow-origin: *; modifyHeaders actions spoofing Referer and Origin for CDN domains.

The code that does this

Rule fetch and application logic (background.js)

Readable version
// background.js:3907-3912 — Remote rule fetchif (!remoteConfig.rulesFetchDate || remoteConfig.rulesFetchDate + 86400000 < Date.now()) {  fetch('https://4saved.com/domains.json?v=1')    .then(r => r.json())    .then(rules => {      if (rules && rules.length) {        store.commit('SET_RULES', rules);        store.commit('SET_RULES_FETCH_DATE', Date.now());      }    });}// background.js:7661-7678 — Application to browserclass DynamicRules {  setRules() {    chrome.declarativeNetRequest.getDynamicRules().then(existingRules => {      chrome.declarativeNetRequest.updateDynamicRules({        addRules: store.state.storage.remoteConfig.rules,        removeRuleIds: existingRules.map(r => r.id)      });    });  }}// Triggered by Vuex mutation: store.subscribe(mutation => {//   if (mutation.type === 'remoteConfig/SET_RULES') dynamicRules.setRules();// });
Remote rule source
    • 4saved.com

    Operator-controlled server serving the declarativeNetRequest rule array; same domain as the extension's name. Rules fetched every 24 hours and applied to all sites.

What the observed rules do

Our dynamic analysis observed rules that: remove the Content-Security-Policy and X-Frame-Options response headers from any site; set Access-Control-Allow-Origin to * on responses; and replace Referer and Origin headers on requests to CDN domains. Stripping CSP weakens the security of every site you visit during the session. Because these rules are fetched from a remote server, their content can change between browser sessions without any extension update.

Remote-controlled m3u8 domain whitelist broadens webRequest inspection scope

Our dynamic analysis captured a startup GET to 4saved.com/master.json.

Its 'master-m3u8-domains' array, stored as remoteConfig.rawMasterDomains, sets which responses get inspected for m3u8 content.

Observed: ['twimg.com', 'hls.ted.com'].

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

You open any browser session with the extension installed.

The service worker runs at startup and fetches the operator-controlled domain list.

The extension did this

The extension downloads a domain list from 4saved.com and uses it to decide which of your network responses to inspect for video content.

Any domain added to the server-side list will subsequently trigger additional fetching and parsing of matching network responses, without a browser extension update.

Captured request
GEThttps://4saved.com/master.json

Returns a JSON object with key 'master-m3u8-domains' containing an array of domain strings. Observed response: {"master-m3u8-domains": ["twimg.com", "hls.ted.com"]}.

The code that does this

Whitelist fetch and webRequest matching (background.js)

Readable version
// background.js:3900-3905 — Whitelist fetchif (!remoteConfig.rawMasterDomainsFetchDate || remoteConfig.rawMasterDomainsFetchDate + 86400000 < Date.now()) {  fetch('https://4saved.com/master.json')    .then(r => r.json())    .then(data => {      store.commit('SET_RAW_MASTER_DOMAINS',        data && data['master-m3u8-domains'] ? data['master-m3u8-domains'] : []);      store.commit('SET_RAW_MASTER_DOMAINS_FETCH_DATE', Date.now());    });}// background.js:7293 — webRequest matchingfor (let i = 0; i < remoteConfig.rawMasterDomains.length; i++) {  if (request.url.includes(remoteConfig.rawMasterDomains[i])) {    matchedRemoteDomain = true;  }}// matchedRemoteDomain triggers additional fetch + m3u8 playlist parsing
Remote whitelist source
    • 4saved.com

    Operator-controlled server serving the m3u8 domain whitelist, same domain as the extension brand. Fetched every 24 hours; the list sets which responses get retrieved.

interceptor.js injected as page script patches XMLHttpRequest on all sites

On every page except YouTube and 4saved.com, the content script injects interceptor.js, a resource any site can load.

It patches XMLHttpRequest's open/send/setRequestHeader to log XHR URLs, read bodies, and parse responses for video URLs.

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

You navigate to any website (other than YouTube and 4saved.com).

The content script, which runs at document_start on all URLs, calls setInterceptor() unconditionally.

The extension did this

The extension injects interceptor.js as a page-level script that replaces the browser's XMLHttpRequest with a custom version that reads every request URL and response body.

The patched XHR logs each request and, for known video-hosting domains, parses the response body to extract video stream URLs.

The code that does this

Script injection and XHR monkey-patch (content.js + interceptor.js)

Readable version

content.js — interceptor injection (content.js:5777-5780)

content.js
// Injects interceptor.js into the page's own JavaScript contextconst script = document.createElement('script');script.src = chrome.runtime.getURL('interceptor.js');script.onload = function() { this.remove(); };(document.head || document.documentElement).appendChild(script);

interceptor.js — XHR prototype monkey-patch (interceptor.js:405-435)

interceptor.js
// Replaces the native XHR prototype methods on the pageconst proto = XMLHttpRequest.prototype;const origOpen = proto.open;const origSend = proto.send;const origSetHeader = proto.setRequestHeader;proto.open = function(method, url) {  this._method = method;  this._url = url;  this._requestHeaders = {};  this._startTime = new Date().toISOString();  return origOpen.apply(this, arguments);};proto.setRequestHeader = function(key, value) {  this._requestHeaders[key] = value;  return origSetHeader.apply(this, arguments);};proto.send = function(body) {  this.addEventListener('load', function() {    if (this._url) {      // Iterate registered origin handlers      for (let i = 0; i < handlers.length; i++) {        for (let j = 0; j < handlers[i].origins.length; j++) {          let url = this._url;          if (!url.startsWith('http')) url = document.location.origin + url;          if (            (handlers[i].origins[j] instanceof RegExp && document.location.hostname.match(handlers[i].origins[j]))            || document.location.hostname === handlers[i].origins[j]          ) {            // Pass full response text to the domain-specific handler            handlers[i].onLoad(this.responseText, url);            break;          }        }      }    }  });  return origSend.apply(this, arguments);};
Data read from each intercepted XHR response
  • Request URL
    https://www.instagram.com/api/v1/media/3491827364/info/

    The full URL of every XHR request made by the page is recorded in the patched prototype.

  • Response body
    {"items":[{"video_versions":[{"url":"https://scontent-lhr8-1.cdninstagram.com/v/t50.2886-16/...","width":1080,"height":1920}]}]}

    For requests from instagram.com, vimeo.com, x.com, twitter.com, and configured CDNs, the full response text is parsed for video stream URLs.

  • Request headers
    {"Authorization": "Bearer IGQWRPa...", "X-CSRFToken": "abc123"}

    All custom request headers set by the page before each XHR send are captured in this._requestHeaders.

Origin patterns subject to response body parsing
    • www.instagram.com

    XHR responses parsed for video_versions JSON keys; extracted video CDN URLs dispatched to the content script.

    • vimeo.com

    XHR responses to /config paths parsed for progressive and HLS video URLs.

    • x.com / twitter.com

    XHR responses parsed for video_info.variants arrays to extract tweet video URLs.

    • hls.enjoy24cdn.com / 928hd.tv / showhd9.com

    XHR responses parsed for #EXTM3U m3u8 content to list available stream qualities.

Updated 30 September 2026pjdbebkgllbkjfbbbbceeeenhkngnddp