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
45Risk

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

Publishers can request a review.

Findings

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-829
SourceAI SANDBOX

Ads Hunter installs server-controlled redirect rules

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.

01EvidenceCAUSE EFFECT
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.

02EvidenceNETWORK CAPTURE
Captured request
POSThttps://adshunter.org/adshunter.php
The source code expects a JSON response with a top-level rules array.
Headers
Content-Typeapplication/json
03EvidenceFIELD TABLE
Rule fields accepted or added by the extension
FieldValueWhy it matters
Remote rules list
rulesLets the server choose which browser network rules the extension will apply after a refresh.
Redirect action
redirectAllows 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 rulesKeeps the returned rule configuration available for later refreshes, not just the current browser session.
04EvidenceCODE COMPARE
The code that does this

The rule-refresh path and broad redirect expansion

What it actually does
API endpoint and request headershelper/helper.js
const API_URL = "https://adshunter.org";
const HEADERS = { "Content-Type": "application/json" };
Readable POST and response validation pathhelper/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 expansionhelper/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 pathbackground.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);
  }
}
05EvidenceTHIRD PARTY LIST
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.

Updated 17 September 2026nglpibmgabflgkjfaaejmilcekemdnkd