Is Ads Hunter safe?

Medium risk

Ads Hunter is medium risk. Ads Hunter requests rule config from adshunter.org/adshunter.php, accepts any response with a rules array, adds a broad URL-match to redirect rules, and installs them as dynamic rules with no signature check or destination allowlist.

Century Labv1.4.5Chrome Web Store
48Risk

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

Publishers can request a review.

Findings

Ads Hunter installs server-controlled redirect rules

MediumunwantedCWE-829Dynamic sandbox

Ads Hunter requests rule config from adshunter.org/adshunter.php, accepts any response with a rules array, adds a broad URL-match to redirect rules, and installs them as dynamic rules with no signature check or destination allowlist.

What actually happens

You did this

Ads Hunter refreshes its network rules from adshunter.org.

The refresh runs on install or update and when the stored refresh interval has expired.

The extension did this

The returned rules are expanded and installed as browser network rules.

Redirect rules receive a broad URL-matching pattern before installation.

Captured request

POSThttps://adshunter.org/adshunter.php

The source code expects a JSON response with a top-level rules array.

Headers
Content-Type
application/json

Rule fields accepted or added by the extension

  • Remote rules list
    rules

    Lets the server choose which browser network rules the extension will apply after a refresh.

  • Redirect action
    redirect

    Allows a matching web request to be sent to a destination supplied by the rule.

  • Broad URL pattern
    ^http.+

    Can apply to many HTTP and HTTPS page or subresource requests instead of a narrow set of sites.

  • Stored rule copy
    chrome.storage.local key rules

    Keeps the returned rule configuration available for later refreshes, not just the current browser session.

The code that does this

The rule-refresh path and broad redirect expansion

Readable version

API endpoint and request headers

helper/helper.js
const API_URL = "https://adshunter.org";const HEADERS = { "Content-Type": "application/json" };

Readable POST and response validation path

helper/helper.js
export async function fetchData() {  try {    const postKeys = ["hunterinfo", "hunterturn", "hunterlastUpdateTime", "hunterstartTabs", "hunterextid", "hunterextv", "hunteran", "huntercid", "huntersid", "visitedDomains", "allowedDomains", "blockedDomains"];    const hunteri = await storage.get(postKeys);    const response = await fetch(`${API_URL}/adshunter.php`, {      method: "POST",      headers: HEADERS,      body: JSON.stringify(hunteri),    });    if (!response.ok) {      setRequestInProgress(false);      setRetryAgain(true);      throw new Error(`Failed to fetch data: ${response.status}`);    }    const text = await response.text();    logger("Response from API:", text);    if (response.headers.get("Content-Type")?.includes("application/json")) {      const data = JSON.parse(text);      logger("Data fetched from API:", data);            if (!data || !Array.isArray(data.rules)) {        throw new Error("Invalid API payload: missing 'rules' array.");      }            setRequestInProgress(false);      setRetryAgain(true);      for (const [k, v] of Object.entries(data)) await storage.updateKey(k, v);      logger(        "Data fetched and stored:",        await storage.get(null)      );      await storage.set({ hunterlastUpdateTime: Date.now() });      await util.uninstallUrlGenerator();      return data;    } else {      setRequestInProgress(false);      setRetryAgain(true);      return null;    }  } catch (err) {    console.error("Error fetching data from API:", err);    return null;  }}

Readable redirect-rule expansion

helper/helper.js
function expandWithConditions(rule, state) {    const {      blockedDomains,      recentlyRemovedBlockedDomains,      allowedDomains,      recentlyRemovedAllowedDomains,    } = state;    let newRule = { ...rule };    newRule.condition = newRule.condition || {};    // requestDomains    let reqDomains = [      ...(Array.isArray(newRule.condition.requestDomains)        ? newRule.condition.requestDomains        : []),      ...(blockedDomains || []),    ];    reqDomains = exclude(reqDomains, recentlyRemovedBlockedDomains);    if (reqDomains.length) {      newRule.condition.requestDomains = reqDomains;    } else {      delete newRule.condition.requestDomains;    }    // excludedRequestDomains and excludedInitiatorDomains    const addExcluded = (existing, whitelist, removals) =>      exclude(        [...existing, ...(whitelist || [])],        [...removals, ...(blockedDomains || [])]      );    const excReq = addExcluded(      newRule.condition.excludedRequestDomains || [],      allowedDomains,      recentlyRemovedAllowedDomains    );    if (excReq.length > 0) {      newRule.condition.excludedRequestDomains = excReq;    } else {      delete newRule.condition.excludedRequestDomains;    }    const excInit = addExcluded(      newRule.condition.excludedInitiatorDomains || [],      allowedDomains,      recentlyRemovedAllowedDomains    );    if (excInit.length > 0) {      newRule.condition.excludedInitiatorDomains = excInit;    } else {      delete newRule.condition.excludedInitiatorDomains;    }    if (newRule.action.type === "redirect") {      newRule.condition.regexFilter = "^http.+";    }    newRule.id = newRule.action.type === "redirect" ? redirectId++ : blockId++;    return [newRule];  }

Readable dynamic-rule installation path

background.js
async function buildAllRules(force = false) {  helper.RulesEngine.resetCounters();  // Sync metadata  await storage.set({    hunterextid: chrome.runtime.id,    hunterextv: chrome.runtime.getManifest().version,  });  // Gather all needed state in one shot  const state = await storage.get([    "allowedDomains",    "recentlyRemovedAllowedDomains",    "blockedDomains",    "recentlyRemovedBlockedDomains",  ]);  // Pipeline!  let rules = await helper.RulesEngine.fetchDynamicRules(    force,    () => chrome.declarativeNetRequest.getDynamicRules(),    helper.fetchData  );  rules = await helper.RulesEngine.removeDefaults(rules, () =>    storage.get("rules")  );  if (!rules.length) return [];  // Expand each rule, flatten result  let expanded = rules.flatMap((rule) =>    helper.RulesEngine.expandRule(rule, state)  );  // Ensure allowAllRule if necessary  expanded = helper.RulesEngine.ensureAllowRule(    expanded,    state.allowedDomains || []  );  return expanded;}async function installRules(adjusted) {  try {    // Add defaults using RulesEngine (mutates adjusted in place)    await helper.RulesEngine.addDefaultRules(      adjusted,      () => storage.get("rules"),      () => storage.get("blockedDomains"),      () => storage.get("allowedDomains")    );    const prev = await chrome.declarativeNetRequest.getDynamicRules();    const idsToRemove = prev.map((r) => r.id);    const payload = idsToRemove.length      ? { removeRuleIds: idsToRemove, addRules: adjusted }      : { addRules: adjusted };    await chrome.declarativeNetRequest.updateDynamicRules(payload);  } catch (e) {    console.error("Failed to update rules:", e);  }}

External host involved in the rule-refresh path

    • adshunter.org

    Receives rule-refresh POST requests and returns the rules that the extension installs as Chrome dynamic network rules.

What it can do

Permissions this extension asks for, as declared in version 1.4.5. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.

  • Read and change your data on every site you visit

    <all_urls>

  • Schedule its own background tasks

    alarms

  • Store data in your browser

    storage

  • See the address and title of every tab you have open

    tabs

  • Block and redirect the requests your browser makes

    declarativeNetRequest

  • See every page you navigate to, as you navigate to it

    webNavigation

declarativeNetRequestWithHostAccess
Updated 21 September 2026nglpibmgabflgkjfaaejmilcekemdnkd