Is Pop up blocker for Chrome™ - Poper Blocker safe?
Poper Blocker sends browsing data (URL, referrer, clicked links) to its own servers and pulls remote scraping rules that drive DOM data extraction.
On each page navigation, the extension POSTs the current URL, referrer, and clicked links to api2.poperblocker.com, encoded with a ROT47 cipher. Separately, it fetches a remote configuration from api2.poperblocker.com every 60 seconds that defines DOM scraping rules — including URL matchers, content matchers, and data extraction pipelines — which content scripts then execute against the active page and return results to the service worker.
Who publishes itdingosolutions - no other listings under this identity
dingosolutions - no other listings under this identity
What this publisher told the store about itself, and the other listings that told it the same thing.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Remote Config Delivers DOM Scraping Instructions on Startup
After install, Poper Blocker fetches a config from api2.poperblocker.com via a hardcoded session ID.
The config is a DSL: query DOM, read/write storage, fetch URLs, upload content.
Re-fetched every 60s; the operator can push updates.
- Severity
- Critical unwanted
- Type
- Unexpected
- CWE
- CWE-829
- Source
- Dynamic sandbox
You install Poper Blocker or restart your browser.
The extension's service worker starts and immediately initiates remote config fetch.
The extension POSTs a hardcoded session ID to api2.poperblocker.com and downloads a config object that controls what the extension does on your browser.
The config acts as a DSL: it can instruct the extension to query DOM elements, read storage, fetch URLs, compress data, and upload results to remote endpoints. It is refreshed every 60 seconds.
On every browser startup
The config fetch fires on every browser startup and extension install, confirmed as the second network request from the service worker before any user interaction, observed during dynamic analysis.
200 OK, JSON object containing DSL keys indexed by integer, stored as priority_block_selectors in chrome.storage.local
- Content-Type
- application/json
{ "sid": "aeb204c39"}Config fetch and 60-second polling loop (FrfRC.uDa module)
uDa module — config fetch + polling (annotated)
// FrfRC.uDa: remote config fetcher moduleFrfRC.uDa = { init: function(deps) { const module = FrfRC.uDa; const storage = deps.instance; // FVK storage helper module.class = class uDa { // Called on startup — begins the 60-second polling loop startPolling() { this.fetchAndApply(); setInterval(() => { this.fetchAndApply(); }, 60000); // every 60 seconds } // Core polling step: check if refresh needed, then fetch async fetchAndApply() { if (await this.shouldRefresh()) { try { const config = await this.fetchConfig(); if (config) { await storage.storeConfig(config); // save to chrome.storage.local this.dispatchConfigEvent(config); // notify other modules via 'olyIG' event } } catch (e) { /* silent failure */ } } } // Broadcast config to other FrfRC modules via CustomEvent dispatchConfigEvent(config) { const ev = new Event('olyIG'); ev.config = config; self.dispatchEvent(ev); } // Fetch config from server using hardcoded session ID async fetchConfig() { const resp = await fetch('https://api2.poperblocker.com/content/config', { method: 'POST', body: JSON.stringify({ sid: 'aeb204c39' }) // hardcoded — same for all users }); if (resp.status === 200) return await resp.json(); return null; } // Returns true if config is stale (older than 6 hours) or missing async shouldRefresh() { if (!await storage.isLoggedIn()) return await storage.isGuestAllowed(); const lastFetch = await storage.getLastFetchTime(); return Date.now() - lastFetch > 21600000; // 21600000ms = 6 hours } }; module.instance = new module.class; module.instance.startPolling(); }, deps: ['FVK']};ZsA module — version/config fetch on startup (annotated)
// ZsA module: version-based config refresh — fetches /version/config 3.3s after startupasync fetchVersionConfig() { const DELAY_MS = 3310; await new Promise(resolve => setTimeout(resolve, DELAY_MS)); if (!await this.isCacheValid()) { const resp = await fetch('https://api2.poperblocker.com/version/config', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ sid: 'aeb204c39', // hardcoded session ID, same for all users hash: currentHash // hash of currently cached config }) }); const newConfig = await resp.json(); if (typeof newConfig === 'object' && Object.keys(newConfig).length) { await this.saveConfig(newConfig); // stored as 'priority_block_selectors' in chrome.storage.local } }}Stores the operator's remote instructions so they survive restarts. The server can update them anytime; checked every 60 seconds.
- Location
- chrome.storage.local key 'priority_block_selectors'
{ "ttl_ms": 86400000, "structure": "Numeric-keyed object — integer indices map to strings used as DOM selectors, URL matchers, property names, and event topics throughout the FrfRC DSL engine", "example_keys": { "224": "site_url_field", "225": "tab_id_field", "267": "x-s", "268": "x-c" }, "cache_hash_key": "priority_block_selectorsh", "cache_timestamp_key": "priority_block_selectorst (base-34 encoded Unix ms)"}- api2.poperblocker.com
Primary config/data endpoint run by Poper Blocker (poper.app). Serves the DSL via /content/config (60s poll) and /version/config (startup); also receives history via /view/update.
Browsing History Transmitted via ROT47-Encoded POST
Every page you visit is sent to Poper Blocker with no consent prompt.
The body is ROT47-encoded, reversible, not encryption.
Captured: page URL, referrer, a session ID, locale, extension version, on every navigation, no visual indication.
- Severity
- Critical unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You visit any website while Poper Blocker is installed.
The extension monitors every page navigation, including pages with no popups to block.
The extension encodes your current URL, referrer, and session ID with ROT47 and POSTs them to api2.poperblocker.com.
This fires on every navigation event as long as the opt-in flag is active, which is set during the onboarding flow.
- Page URLhttps://www.nytimes.com/2025/03/15/world/us-canada-trade-tariffs.html
The full address of every page you visit, letting the operator build a complete browsing history.
- Referrer URLhttps://www.google.com/search?q=canada+tariffs+2025
Where you came from before this page, reveals navigation patterns and search queries.
- Session IDaeb204c39
A persistent identifier that ties all your navigation events together across visits and browser restarts.
- Extension version8.1.0
The installed version of Poper Blocker, used for server-side targeting of config changes.
- Localeen-US
Your browser language setting, used for user segmentation.
- Timestamp1744586423851
Exact millisecond timestamp of each navigation, enabling precise timeline reconstruction.
The POST body is ROT47-encoded, every ASCII character is rotated by half the printable character set (47 positions), making the URL and payload unreadable at a glance in browser DevTools or network monitors.
{ "u": "https://www.nytimes.com/2025/03/15/world/us-canada-trade-tariffs.html", "p": "https://www.nytimes.com/2025/03/15/world/us-canada-trade-tariffs.html", "kk": "https://www.google.com/search?q=canada+tariffs+2025", "us": "aeb204c39", "nid": "8.1.0", "t": 1744586423851, "lc": "en-US", "t2": "article", "edh": {}}ROT47 encoder shipped in the extension
// Wr.Rotate: ROT47 cipher — used to encode the browsing-history POST bodyWr.Rotate = class { // 94 printable ASCII characters in order (the rotation alphabet) static get map() { return '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'; } // Entry point: JSON-serialise the payload then ROT47-encode it static rotate(obj) { if (this.isEmpty(obj)) throw new Error(); return this.rot47(JSON.stringify(obj)); } // Rotate each printable character by 47 positions within the 94-char alphabet static rot47(str) { let result = ''; const mapLen = this.map.length; // 94 for (let i = 0; i < str.length; i++) { const ch = str.charAt(i); const idx = this.map.indexOf(ch); result += idx >= 0 ? this.map.charAt((idx + mapLen / 2) % mapLen) : ch; } return result; } // Guard: reject null/empty inputs before encoding static isEmpty(val) { if (val == null) return true; if (val.length > 0) return false; if (val.length === 0) return true; if (typeof val !== 'object') return true; for (var k in val) if (Object.prototype.hasOwnProperty.call(val, k)) return false; return true; }};Decodes the ROT47-encoded POST body that Poper Blocker sends to api2.poperblocker.com/view/update. Paste any captured request body to see the plain-text JSON containing your visited URLs.
- Node.js 12+
#!/usr/bin/env node// rot47-decode.js// Decodes the ROT47-encoded body from Poper Blocker's api2.poperblocker.com/view/update POST.//// Usage:// node rot47-decode.js// Then paste/type the encoded body and press Ctrl+D (Unix) or Ctrl+Z Enter (Windows).//// Or pipe a captured body directly:// echo '<encoded-body>' | node rot47-decode.js//// No dependencies required — pure Node.js.const MAP = '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~';const MAP_LEN = MAP.length; // 94function rot47(encoded) { let result = ''; for (let i = 0; i < encoded.length; i++) { const ch = encoded[i]; const idx = MAP.indexOf(ch); result += idx >= 0 ? MAP[(idx + MAP_LEN / 2) % MAP_LEN] : ch; } return result;}function main() { const chunks = []; process.stdin.on('data', chunk => chunks.push(chunk)); process.stdin.on('end', () => { const input = Buffer.concat(chunks).toString('utf8').trim(); if (!input) { console.error('Error: no input. Pipe encoded body via stdin or paste and press Ctrl+D.'); process.exit(1); } const decoded = rot47(input); let parsed; try { parsed = JSON.parse(decoded); } catch { // Not valid JSON after decode — print raw decoded string console.log('Decoded (not valid JSON):'); console.log(decoded); return; } console.log('Decoded POST body (Poper Blocker view/update):'); console.log(JSON.stringify(parsed, null, 2)); console.log(); console.log('Key fields:'); if (parsed.u) console.log(' Visited URL :', parsed.u); if (parsed.kk) console.log(' Referrer URL :', parsed.kk); if (parsed.us) console.log(' Session ID :', parsed.us); if (parsed.nid) console.log(' Extension ver:', parsed.nid); if (parsed.t) console.log(' Timestamp :', new Date(parsed.t).toISOString()); if (parsed.lc) console.log(' Locale :', parsed.lc); });}main();- 1echo '<encoded-body>' | node rot47-decode.js
- api2.poperblocker.com
Primary data endpoint run by Poper Blocker (poper.app). Receives every visited URL plus a persistent session ID; also serves remote config on startup (claim #5849).
Poper Blocker Reads claude.ai and google.com Content Against Remote Selectors
On claude.ai and google.com, Poper Blocker matches DOM against a selector list from api2.poperblocker.com/content/config, including AI chat and search results.
Matches increment a per-tab counter, batched to GA4 and the vendor's analytics.
- Severity
- Critical unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You open a conversation on claude.ai or a page on google.com.
Added to the content-script scope in version 8.1.0; the same script already ran on Facebook, LinkedIn, X, Instagram, Reddit, Pinterest, YouTube, and chatgpt.com.
The extension reads elements on the page and checks each one against a selector list it downloaded from its own server, then counts the matches.
The selector list (filterPost) is delivered at runtime, so which elements are matched is decided by the server, not by code shipped in the extension.
How the extension decides which page elements to react to (from the shipping source).
ClaudeNetwork.getPostContentElements
// ClaudeNetwork (runs on claude.ai). Returns the element if it matches// any selector in the server-delivered filterPost list.getPostContentElements = (el) => { if (!(el instanceof HTMLElement)) return null; const selectors = this.socialConfig.querySelectorsOffsetData.filterPost; // from api2.poperblocker.com return selectors.some(sel => el.matches(sel)) ? [el] : null;};SocialNetworkManager.tryBlockContent
// SocialNetworkManager.tryBlockContent (shared by claude.ai and google.com).// Reads the page element, checks it against the remote selectors, then// counts the match.tryBlockContent = async (el) => { const matched = this.socialNetwork.getPostContentElements(el); if (matched && KeywordStore.shouldBlock(matched) && this.socialNetwork.isMainFeed(el)) { this.socialNetwork.hidePost(el, true, Feature.SOCIAL_CONTENT_BLOCKER); }};Returns the per-site configuration including querySelectorsOffsetData.filterPost, the CSS-selector list used to decide which DOM elements on claude.ai and google.com are matched. The list is server-controlled and can change without an extension update.
- Content-Type
- application/json
{ "sid": "aeb204c39"}on match
You didA DOM element on claude.ai matches a server-delivered selector.
The extension didSends update-block-stat (type 'keywords') to the background script.
queued
You didBackground script receives update-block-stat.
The extension didIncrements a per-tab counter recorded with the page host (e.g. claude.ai).
on flush
You didAnalytics event assembly reads the accumulated counters.
The extension didPosts the counters to Google Analytics 4 and analytics.poperblocker.com.
The Google Analytics 4 reporting endpoint's measurement_id and api_secret are not stored in plain text in the source; they are base64-encoded three times before use, so they do not appear when searching the code for the destination.
measurement_id = G-0FCR6EG7BPapi_secret = <redacted>endpoint = https://www.google-analytics.com/mp/collect?measurement_id=G-0FCR6EG7BP&api_secret=<redacted>- api2.poperblocker.com
Delivers the per-site selector list (filterPost) that decides which DOM elements on claude.ai and google.com are matched. Operated by the extension developer (Poper Blocker).
- analytics.poperblocker.com
Developer-operated analytics endpoint configured as ANALYTICS_APP for usage reporting.
- www.google-analytics.com
Google Analytics 4 Measurement Protocol endpoint (measurement_id G-0FCR6EG7BP) receiving the batched per-tab counters, including the page host on which matches occurred.
Decodes the triple-base64-encoded Google Analytics 4 measurement_id and api_secret used by the extension, so you can confirm the reporting destination yourself from the shipping source.
- Node.js 18+ or any browser DevTools console
// poper-ga4-decode.js// Reproduces the decode() function from service-worker.js (line 4722)// and applies it to the two encoded constants used in ga4Event().const decode = (e) => atob(atob(atob(e)));const measurementIdEnc = 'VW5rd2QxSnJUbE5PYTFaSVRqQktVUT09';const apiSecretEnc = 'Vld4c05tVnRTbE5PVlVaVlUxZHNTMVZXU2tWa01rcHlUbGhLY2xGUlBUMD0=';console.log('measurement_id =', decode(measurementIdEnc));console.log('api_secret =', decode(apiSecretEnc));console.log('endpoint = https://www.google-analytics.com/mp/collect' + '?measurement_id=' + decode(measurementIdEnc) + '&api_secret=' + decode(apiSecretEnc));- 1Save as poper-ga4-decode.js.
- 2Run: node poper-ga4-decode.js (or paste into any browser DevTools console).
- 3Compare the printed values against the encoded strings in service-worker.js around the ga4Event function.
+4 more findings not shown
Where it sends data
Destinations our analysis observed Poper 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.
- api2.poperblocker.com
Poper Blocker sends data to api2.poperblocker.com. One other extension we have analysed sends data here.