Is Popup Blocker - Blocks annoying Popups safe?

Critical risk

Popup Blocker fetches per-domain JavaScript from popupsblocker.org and runs it in the main page context of any site you visit.

The extension's service worker pulls a configuration from popupsblocker.org on install and every 24 hours, then stores per-domain JavaScript strings that its content script injects into the main world of matching pages via chrome.scripting.executeScript. Because the server can replace those stored strings (and the extension's entire network-filtering ruleset) at any time, the code that runs on your pages is fully server-controlled. It also assigns a persistent server-issued user ID and uploads your sync-storage counters (total page loads plus Google, Bing, and Yahoo search counts) to the same server on each update cycle.

PopUp Blockerv2026.5.22.17Chrome Web Store
97Risk
Who publishes it

Popup Blocker - no other listings under this identity

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
PopUp Blocker
Declared legal entity
Popup Blocker
Registered address
Pater-Schwartz-Gasse 11A, Wien 1150, AT
Registered contact
George Dubois

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

Publishers can request a review.

Findings

Server-Controlled JavaScript Injected Into All Visited Pages

Popup Blocker fetches JS strings from popupsblocker.org and stores them locally.

Each visit sends your domain to the service worker, which injects the matching script into the MAIN world.

Testing found 32 domain scripts run on first visit.

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

You navigate to any web page, any site.

The extension did this

The extension injects JavaScript into the page's MAIN execution context using a script fetched from its server at install time.

The injected code runs in the same environment as the page itself, with access to the page's DOM, forms, and JavaScript globals.

What's stored on your device

JavaScript keyed by domain, loaded from the server. Runs on that site when visited, and can be swapped by the server at any update.

Location
chrome.storage.local (populated by server response at install)
Contents
{  "google": "(() => { const nuke = () => { document.querySelectorAll('#tads, .uEierd, .commercial-unit-desktop-top, .commercial-unit-desktop-right, .ads-ad, [aria-label=\"Ads\"]').forEach(el => el.style.display = 'none'); }; setInterval(nuke, 500); })();",  "default": "console.log('[AdBlock] Standard protection enabled.');",  "syncuid": "a7f3b9c2-1e4d-4f8a-b3c1-9e2d5f7a3b1c",  "twitter": "(() => { const nuke = () => { document.querySelectorAll('[data-testid=\"tweet\"]').forEach(tweet => { if (tweet.innerText.includes('Promoted')) { const cell = tweet.closest('[data-testid=\"cellInnerDiv\"]'); if(cell) cell.style.display = 'none'; } }); document.querySelectorAll('[data-testid=\"sidebarColumn\"] [data-testid=\"trend\"]').forEach(t => { if(t.innerText.includes('Promoted')) t.style.display='none'; }); }; setInterval(nuke, 1000); })();",  "facebook": "(() => { const nuke = () => { document.querySelectorAll('div[aria-label=\"Sponsored\"]').forEach(el => { const card = el.closest('[role=\"feed\"] > div, [data-pagelet^=\"FeedUnit_\"]'); if (card) card.style.display = 'none'; }); document.querySelectorAll('iframe[src*=\"facebook.com/plugins/like.php\"]').forEach(el => el.remove()); }; setInterval(nuke, 1000); })();",  "instagram": "(() => { const nuke = () => { document.querySelectorAll('article').forEach(article => { const label = article.querySelector('span'); if (label && (label.innerText === 'Sponsored' || label.innerText === 'Paid partnership')) article.style.display = 'none'; }); }; setInterval(nuke, 1000); })();"}
Captured request
GEThttps://popupsblocker.org/jsonhybrid17-dev.php?c=uid&version=1175&extid=dhemafmfialpibmahglbfgjihhfheepp

JSON object with 32 domain-keyed JS script strings plus a syncuid. The extension stores all values directly to chrome.storage.local and marks the unique user ID in both sync and local storage.

The code that does this

The injection path from content script to MAIN world execution.

Readable version

content_script.js — fires on every page at document_start (already readable)

// Runs before the page's own scripts execute.// Sends the current hostname to the service worker.// Works in all frames (iframes too), not just the top-level page.chrome.runtime.sendMessage({    action: "CHECK_RULES",    domain: window.location.hostname,   // e.g. 'google.com'    isTop: window === window.top         // true if this is the main frame});

serviceworker.js — handleRuleCheck() injects the server-supplied code

// ruleContent is a JS string fetched from chrome.storage.local.// It was originally loaded from the server on install and can be// replaced by a subsequent server sync (every 24 hours).//// world: 'MAIN' means the code runs in the same JS context as// the page itself — it can read passwords, intercept form submits,// access page-level variables, etc.chrome.scripting.executeScript({    target: { tabId: tabId, frameIds: [frameId] },    func: secureApply,         // inlines ruleContent as a <script> tag    args: [ruleContent],       // the JS string from storage    world: 'MAIN',             // full page environment access    injectImmediately: true    // runs before page scripts});
Why MAIN world matters

Chrome's `scripting` API offers two execution worlds: `ISOLATED` (the default) and `MAIN`. Code in the `ISOLATED` world runs in a sandboxed context with access to the DOM but not to the page's own JavaScript variables and functions. Code injected into the `MAIN` world shares the same JavaScript environment as the page — meaning it can read form field values, intercept authentication tokens held in memory, hook into page-level event handlers, and interact with any JavaScript library loaded by the site.

For an ad blocker, `MAIN` world access is not required — DOM element hiding can be done from `ISOLATED` world. The choice to use `MAIN` world injection means the server-supplied code has the broadest possible access to each page you visit.

Server that controls what code runs on your pages
    • popupsblocker.org

    Primary server. Supplies JS strings run in the MAIN world on each navigation and controls declarativeNetRequest rules. Sends a unique syncuid on first contact.

Check if you're affected

Run this in Chrome DevTools while the extension is active. It hooks chrome.scripting.executeScript in the service worker context to log each time the extension injects code into the MAIN world, printing the target tab, domain key, and the code string about to be executed.

Requires
  • Chrome with Developer mode enabled
popup-blocker-main-world-detector.js · js
// popup-blocker-main-world-detector.js// Paste into the Popup Blocker service worker DevTools console.// (chrome://extensions → enable Developer mode → click the service//  worker link for Popup Blocker)(function() {  const orig = chrome.scripting.executeScript.bind(chrome.scripting);  chrome.scripting.executeScript = function(params, ...rest) {    if (params && params.world === 'MAIN') {      const code = params.args && params.args[0];      console.group('[POPUP_BLOCKER_DETECT] MAIN world injection');      console.log('Target tab:', params.target?.tabId);      console.log('Frame IDs:', params.target?.frameIds);      console.log('Code to be injected (first 500 chars):', String(code).slice(0, 500));      console.groupEnd();    }    return orig(params, ...rest);  };  console.log('[POPUP_BLOCKER_DETECT] Monitoring chrome.scripting.executeScript for MAIN world injections...');})();
How to run it
  1. 1Open chrome://extensions, enable Developer mode.
  2. 2Open Popup Blocker's service worker DevTools.
  3. 3Paste this script.
  4. 4Visit google.com or a site in the extension's domain list.
  5. 5Watch for [POPUP_BLOCKER_DETECT] entries.

Server Replaces Browser Network Rules at Runtime

On install, update, or daily sync, Popup Blocker asks popupsblocker.org for its rule set.

A 'rules' key replaces all filtering rules immediately, no user action.

Testing captured 4,472 redirect and 29 block rules via updateDynamicRules.

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

The extension installs, updates, or its 24-hour alarm fires.

No user action is required; this happens automatically in the background.

The extension did this

The server replaces all of the extension's active network filtering rules with a new set of its own choosing.

The new rules take effect immediately for all subsequent browser network requests.

Captured request
GEThttps://popupsblocker.org/jsonhybrid17-dev.php?c=uid&version=1175&extid=dhemafmfialpibmahglbfgjihhfheepp

JSON response contained a 'rules' array with 4,472 redirect rules (routing matched domains to the extension's close.html page) and 29 block rules. Also included per-domain JavaScript strings and a syncuid for the new install. Total response was approximately 1,397 characters of rule data.

The code that does this

The remote rule replacement, shipped source vs annotated version.

Readable version

Rule replacement — annotated

// If the server response contains a 'rules' key:if (json.rules) {    // Parse the rules (server may send them as a JSON string or an array)    const newRules = typeof json.rules === 'string'        ? JSON.parse(json.rules)        : json.rules;    // Remove ALL currently active dynamic rules    const currentRules = await chrome.declarativeNetRequest.getDynamicRules();    const removeRuleIds = currentRules.map(rule => rule.id); // collect every active rule ID    // Atomically: delete old rules, install server-provided rules    await chrome.declarativeNetRequest.updateDynamicRules({        removeRuleIds: removeRuleIds,   // wipe slate clean        addRules: newRules              // server now controls what is blocked/redirected    });    // From this point on, the browser's network filter for this extension    // matches exactly what the server sent — nothing more, nothing less.}

URL construction — all sync storage is sent as query parameters

// params is the full chrome.storage.sync contents on update requests.// This means counters (cnt_total, cnt_google, cnt_bing, cnt_yahoo),// the syncuid, and lastUpdated are all serialized into the URL.const url = `https://popupsblocker.org/jsonhybrid17-dev.php` +    `?c=${type}` +                          // 'uid' on install, 'update' on refresh    `&${new URLSearchParams(params)}` +      // full sync storage contents    `&version=${manifestVersion}` +         // extension version    `&extid=${chrome.runtime.id}`;           // extension ID
What server-controlled network rules can do

declarativeNetRequest (DNR) rules are enforced at the browser's network layer — they intercept HTTP/HTTPS requests before any web page code sees them. A server-controlled DNR rule set can:

- **Redirect** requests for any domain to any URL the rule author chooses (including the extension's own pages) - **Block** requests to any domain without notifying the user or the page - **Modify headers** on requests or responses

The 4,472-domain redirect rule set observed in our analysis routes matching domains to the extension's `close.html` page. The 29-domain block list drops requests without a prompt. Because the server can replace these rules at any 24-hour sync, the full set of blocked and redirected domains can change without any reinstall or user notification.

Server that controls the extension's network filter rules
    • popupsblocker.org

    Primary server. Supplies declarativeNetRequest rules for the network layer, plus per-domain JS strings, and assigns a syncuid. Contacted on install, update, and every 24 hours.

Persistent server-assigned user ID tracks you across sessions

On install, the extension gets a unique 72-char ID from its server, stored in Chrome sync and local storage.

Sent with every daily check-in, letting the server link usage across devices.

Also embedded in the uninstall URL.

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

You install the extension for the first time.

No interaction needed, the UID registration happens automatically during the install lifecycle.

The extension did this

The extension requests a permanent tracking identifier from its server and stores it in your browser.

The ID persists in chrome.storage.sync, so it survives extension reinstalls and follows you across every signed-in Chrome profile.

Captured request
GEThttps://popupsblocker.org/jsonhybrid17-dev.php?c=uid&version=11750&extid=dhemafmfialpibmahglbfgjihhfheepp

JSON object containing syncuid: 'sfVA1xqoJIi...' (72-char server-assigned ID) plus optional rule updates.

Data linked to your permanent identifier on every daily check-in
  • Permanent user ID
    sfVA1xqoJIibKm8nPqRx2dLwTcYeUoAzHgFvNsBjMkWlXtDyEpCrQu0v3i7h4O

    A 72-character token assigned by the server that uniquely identifies your browser installation across all future sessions.

  • Total page visits
    cnt_total: 1482

    Running count of every web page you load while the extension is active, sent with each daily update.

  • Search-engine usage
    cnt_google: 204, cnt_bing: 11, cnt_yahoo: 3

    Separate counters for Google, Bing, and Yahoo searches, all tied to your permanent ID.

  • Extension version + runtime ID
    version=11750&extid=dhemafmfialpibmahglbfgjihhfheepp

    Extension version and Chrome's internal runtime ID are appended to every outbound request.

The code that does this

UID registration and storage, serviceworker.js

Readable version
// serviceworker.js:332-393 (abridged)if (!syncuid) {    logger.log('Registering new user ID...');    await syncRemoteConfiguration('uid', '');}// Inside syncRemoteConfiguration, after server JSON is parsed:if (key === 'syncuid') {    return Promise.all([        chrome.storage.sync.set({ [key]: value }),   // survives reinstall via account sync        chrome.storage.local.set({ [key]: value })   // local copy for fast reads    ]);}
The code that does this

Uninstall beacon includes full sync storage, serviceworker.js:410-419

Readable version
async function setUninstallUrl() {    const dataSync  = await chrome.storage.sync.get();          // ALL sync keys    const dataLocal = await chrome.storage.local.get(['syncuid']);    if (dataLocal['syncuid'] && !dataSync['syncuid']) {        dataSync['syncuid'] = dataLocal['syncuid'];    }    const searchParams = new URLSearchParams(dataSync);          // includes syncuid, cnt_total, cnt_google, …    await chrome.runtime.setUninstallURL(        `https://popupsblocker.org/uninstall.php?${searchParams.toString()}`    );}
Endpoints that receive the permanent user identifier
    • popupsblocker.org

    Developer server. Receives UID assignment (?c=uid), daily check-ins (?c=update) with usage metrics, and an uninstall beacon (/uninstall.php) with full sync-storage contents.

Where it sends data

Destinations our analysis observed Popup Blocker contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.

  • popupsblocker.org

    Popup Blocker sends data to popupsblocker.org. No other extension we have analysed sends data here.

Updated 30 September 2026dhemafmfialpibmahglbfgjihhfheepp