Is Ads Hunter safe?
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.
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
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
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 returned rules are expanded and installed as browser network rules.
Redirect rules receive a broad URL-matching pattern before installation.
Captured request
The source code expects a JSON response with a top-level rules array.
- Content-Type
- application/json
Rule fields accepted or added by the extension
- Remote rules listrules
Lets the server choose which browser network rules the extension will apply after a refresh.
- Redirect actionredirect
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 copychrome.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
API endpoint and request headers
helper/helper.jsconst API_URL = "https://adshunter.org";const HEADERS = { "Content-Type": "application/json" };Readable POST and response validation path
helper/helper.jsexport 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.jsfunction 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.jsasync 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
