Is Spotify Ad Blocker - Blockify safe?

High risk

Blockify is high risk. Every 60 minutes, Blockify fetches a config from blockify.b-cdn.net controlling ad-blocking features, exempt sites, and filtering rules. custom_dynamic_dnr injects rules that allow/block requests; forced_exclusions adds exempt domains.…

ValueFoundryv1.9.9.1Chrome Web Store
75Risk
Who publishes it

ValueFoundry - 2 other listings from the same operator, none carrying a finding

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

Same store account

2 other listings published from this account, 4k+ users between them, none of them carrying a finding.

Shared hosts - 1 hostname

Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.

s1.browsebetter.io
Also called by 1 other listing: Spotify Ad Blocker

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

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-829
SourceAI SANDBOX

Remote Configuration Controls Ad Blocking Behavior via blockify.b-cdn.net

Every 60 minutes, Blockify fetches a config from blockify.b-cdn.net controlling ad-blocking features, exempt sites, and filtering rules. custom_dynamic_dnr injects rules that allow/block requests; forced_exclusions adds exempt domains.

01EvidenceCAUSE EFFECT
What actually happens
You did this

The extension starts up, then every 60 minutes thereafter, a background alarm fires.

The extension did this

The extension fetches a JSON configuration file from blockify.b-cdn.net and applies it, potentially changing which sites are blocked or exempt from ad blocking.

The server can add any domain to the ad-blocking exception list (forced_exclusions) or inject arbitrary declarativeNetRequest rules (custom_dynamic_dnr) without any user interaction.

02EvidenceNETWORK CAPTURE
Captured request
GEThttps://blockify.b-cdn.net/switches190.json
HTTP 200 OK. Response body (confirmed live): {"spotify_injection":"enabled","yt_injection":"enabled","hulu_injection":"enabled","cssjs":"enabled","rules_json":"enabled","forced_exclusions":["nil"],"custom_dynamic_dnr":[]}
03EvidenceFIELD TABLE
Configuration fields returned by blockify.b-cdn.net/switches190.json:
FieldValueWhy it matters
forced_exclusions
["example-ad-network.com", "partner-site.com"]A list of domains added to your ad-blocking exception list, bypassing the ad blocker for those sites. Controlled entirely by the server.
custom_dynamic_dnr
[{"id":9001,"priority":1000,"action":{"type":"allow"},"condition":{"urlFilter":"||ads.example.com*"}}]An array of network filtering rules the server can inject into the extension. These rules can allow or block any request from your browser.
rules_json
"disabled"Enables or disables the extension's built-in ad blocking ruleset for all users simultaneously.
cssjs
"enabled"Enables or disables CSS-based ad element hiding across all sites.
spotify/yt/hulu_injection flags
"enabled"Toggle per-site ad blocking for Spotify, YouTube, and Hulu for all users.
04EvidenceCODE COMPARE
The code that does this

Server-controlled DNR rule injection and exception list update in bk_modules.js

What it actually does
Every 60 minutes, the extension downloads a config and applies it without user notification
// bk_modules.js: A background alarm fires every 60 minutes (or on startup)
// and fetches the 'switches' config from blockify.b-cdn.net.
// The server response directly controls:
//   - forced_exclusions: domains exempt from ad blocking (merged into user's exception list)
//   - custom_dynamic_dnr: arbitrary declarativeNetRequest rules injected into the extension
//   - rules_json: enables/disables the main ad blocking ruleset
//   - cssjs, spotify_injection, yt_injection, hulu_injection: per-feature toggles
//
// There is no UI notification when any of these values change.
async function setSwitches() {
  const response = await fetch('https://blockify.b-cdn.net/switches190.json', { cache: 'no-cache' });
  const config = await response.json();
  // Store config and apply immediately:
  chrome.storage.local.set({ switches: config });
  self.switches = config;
  applyForcedExclusions(config.forced_exclusions); // updates your exception list
  applyCustomDNRRules(config.custom_dynamic_dnr);  // injects server-side network rules
  setAdBlockingToggles(config);                    // enables/disables features
}
The server can inject any declarativeNetRequest rule into the extension
// The custom_dynamic_dnr array from the server is passed directly to
// chrome.declarativeNetRequest.updateDynamicRules().
// Rules can have any action (allow, block, redirect, modifyHeaders) and
// any condition (urlFilter, resourceTypes, domains, etc.).
// There is no validation of rule contents before they are applied.
async function applyServerDNRRules(rules) {
  const existing = await chrome.declarativeNetRequest.getDynamicRules();
  const existingIds = new Set(existing.map(r => r.id));
  const newRules = rules.filter(r => !existingIds.has(r.id)); // skip duplicates by ID
  if (newRules.length > 0) {
    await chrome.declarativeNetRequest.updateDynamicRules({ addRules: newRules });
  }
}
05EvidenceTHIRD PARTY LIST
Where the remote configuration is fetched from:
  • blockify.b-cdn.net

    BunnyCDN distribution serving switches190.json. Controlled by the Blockify developer. Configuration changes here affect all ~300K users simultaneously.

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

Browsing History Transmitted to insights.getblockify.com on Every Navigation

Every time you navigate, Blockify sends the full URL, referring URL, install ID, OS, browser name/version, timezone, and user-agent to insights.getblockify.com in 5 encrypted POSTs; the hardcoded key lets anyone decrypt them.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You navigate to any page in any browser tab.

The extension did this

The extension sends the full URL, the page you came from, and a persistent device ID to insights.getblockify.com.

This fires on every completed navigation across all sites, not just Spotify. No action or opt-in is required.

02EvidenceNETWORK CAPTURE
Captured request
POSThttps://insights.getblockify.com/process
HTTP 201 Created (770 bytes body observed during dynamic analysis, 5 such requests captured)
Headers
Content-Typeapplication/json;charset=utf-8
Body
{
  "eventType": 1,
  "request": {
    "enRequest": "Xk9mR2FhQkxPdUZvN1ZhT3ZMdEprQ2FvQ0VscXZUSmVPb2ZJNFFwUW1PZlVyT3FKS1hIck1tVlN0WkV4SHdFWkFud3VhWTZzdmE0djlhZzNaM0tLMGdkQzBkQjNaZFdlR2Y2amhxMVBLUnVxbzFHT1JyMlpFaHl5UW56ekdlNW5GWDZGVG5nYUxEY3dvcXVaUERheVZBZ3VETnJveHpVNkFPbGRhU2drcVhvUzZOak9ib0RqYjZxNlFMeVFJaEdEeDU3VG5DazVpV2FyYThsblBjQkVOZ2V3UnlSUmd4ZzRGR3plczJIN1g0Q0hXNndRZ0Ux"
  }
}
03EvidenceOPAQUE REVEAL
Why you can't catch this in DevTools

The request body is AES-GCM encrypted before sending. The 16-byte encryption key is hardcoded in analytics.js at line 535, so the payload can be fully decrypted by anyone with the extension source.

What's actually being sent
[
  {
    "fileDate": "2026-04-15T03:05:12.441Z",
    "deviceTimestamp": 1744682712441,
    "userId": "e3359683-1ef2-4a0e-b57e-602aec57a0b1",
    "referrerUrl": null,
    "targetUrl": "https://www.google.com/",
    "requestType": "GET",
    "scheme": "https:",
    "host": "www.google.com",
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
    "accept_language": "en-US,en;q=0.9",
    "os_name": "Linux",
    "browser_name": "Not-A.Brand",
    "os_version": "N/A",
    "browser_version": "8",
    "timeZone": "UTC"
  }
]
04EvidenceFIELD TABLE
What Blockify sends on every page navigation:
FieldValueWhy it matters
The URL you visited
https://mail.google.com/mail/u/0/#inboxThe exact address of every page you load, including any query parameters.
The page you came from
https://www.google.com/search?q=gmailThe URL you were on before this page, builds a chain of your browsing session.
Your persistent device ID
e3359683-1ef2-4a0e-b57e-602aec57a0b1A UUID generated once and stored in chrome.storage.sync. It is the same across all sites you visit and survives browser restarts.
Your operating system
macOS 10.15.7The OS name and version detected from your browser environment.
Your browser name and version
Chrome 130The specific browser and version you are using.
Your timezone
America/New_YorkYour local timezone as reported by the browser, narrows your geographic location.
Timestamp
2026-04-15T03:05:12.441ZWhen you visited the page, to the millisecond.
05EvidenceCODE COMPARE
The code that does this

The navigation listener and payload assembly code in analytics.js

What it actually does
Extension initializes analytics with hardcoded API key, encryption key, and server
// The extension creates a PageStatistics tracker with three hardcoded values:
// - API key: "Iengi0kiEi3eicae"
// - Encryption key: "ej0hie1MThoo8Ri2"  ← anyone with the source can decrypt traffic
// - Server: "https://insights.getblockify.com"
self.stat = new PageStatistics("Iengi0kiEi3eicae", "ej0hie1MThoo8Ri2", "https://insights.getblockify.com");
stat.init(); // registers all navigation event listeners
On every page navigation, reportAction() collects and encrypts the visit record
// Called by chrome.tabs.onUpdated for every completed navigation.
// Assembles a visit record with: the visited URL, referrer, persistent UUID,
// OS name/version, browser name/version, timezone, user-agent, and timestamp.
// The record is AES-128-GCM encrypted (key hardcoded above) and POSTed to /process.
async function reportAction(url, referrer) {
  const payload = [{
    fileDate: new Date().toISOString(),       // ISO timestamp
    deviceTimestamp: Date.now(),              // ms precision
    userId: this.data.uuid,                  // persistent UUID from chrome.storage.sync
    referrerUrl: referrer,                   // previous page URL
    targetUrl: url,                          // visited page URL
    requestType: 'GET',
    scheme: new URL(url).protocol,
    host: new URL(url).hostname,
    user_agent: navigator.userAgent,
    accept_language: navigator.language,
    os_name: getOSName(),
    browser_name: getBrowserName(),
    os_version: getOSVersion(),
    browser_version: getBrowserVersion(),
    timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
  }];
  const encrypted = await encryptData(JSON.stringify(payload));
  await fetch('https://insights.getblockify.com/process', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json;charset=utf-8' },
    body: JSON.stringify({ eventType: 1, request: { enRequest: encrypted } })
  });
}
06EvidenceTHIRD PARTY LIST
Where your browsing data is sent:
  • insights.getblockify.com

    Primary telemetry endpoint. Receives every navigation event from every user. Operated by the Blockify developer.

07EvidenceARTIFACT
Reproduce it yourself

Decrypts the AES-128-GCM encrypted payloads that Blockify sends to insights.getblockify.com/process. The hardcoded key from analytics.js:535 is used to decrypt any captured enRequest value. Run with a base64 enRequest as the argument, or without arguments for a self-contained demo.

RequiresNode.js 14+No npm packages required (uses built-in crypto module)
blockify-decrypt.js · js
/**
 * PoC: Decrypt Blockify (nfmlkliedggdodlbgghmmchhgckjoaml) analytics payloads
 *
 * analytics.js:533-536 hardcodes:
 *   api_key       = "Iengi0kiEi3eicae"
 *   encryption_key = "ej0hie1MThoo8Ri2"   (16-byte AES-GCM key)
 *   server_url    = "https://insights.getblockify.com"
 *
 * Encryption scheme (analytics.js:373-398):
 *   key  = AES-GCM import of raw UTF-8 bytes of encryption_key
 *   iv   = 16 random bytes prepended to ciphertext
 *   payload = base64(iv || AES-GCM-encrypt(JSON.stringify([...event objects...])))
 *
 * The decrypted payload is the JSON array sent to /process as enRequest.
 * Usage:
 *   node blockify-decrypt.js [base64_enRequest]
 *
 * If no argument is given, the script demonstrates encryption+decryption with
 * a synthetic payload matching the fields collected in analytics.js:233-264.
 */

const crypto = require('crypto');

const ENCRYPTION_KEY = "ej0hie1MThoo8Ri2"; // analytics.js:535

/**
 * Decrypt a base64-encoded AES-GCM payload (16-byte IV prepended).
 */
function decrypt(base64Payload) {
  const raw = Buffer.from(base64Payload, 'base64');
  const iv = raw.slice(0, 16);
  const ciphertext = raw.slice(16);

  const keyBuf = Buffer.from(ENCRYPTION_KEY, 'utf8'); // 16 bytes = AES-128
  const decipher = crypto.createDecipheriv('aes-128-gcm', keyBuf, iv);

  // AES-GCM auth tag is the last 16 bytes of ciphertext
  const authTag = ciphertext.slice(ciphertext.length - 16);
  const actualCiphertext = ciphertext.slice(0, ciphertext.length - 16);
  decipher.setAuthTag(authTag);

  const decrypted = Buffer.concat([decipher.update(actualCiphertext), decipher.final()]);
  return decrypted.toString('utf8');
}

/**
 * Encrypt a payload the same way the extension does.
 * Returns the base64 string that would appear as enRequest.
 */
function encrypt(plaintext) {
  const keyBuf = Buffer.from(ENCRYPTION_KEY, 'utf8');
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv('aes-128-gcm', keyBuf, iv);
  const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
  const tag = cipher.getAuthTag();
  const combined = Buffer.concat([iv, encrypted, tag]);
  return combined.toString('base64');
}

// --- Demonstration: round-trip a synthetic event matching analytics.js payload shape ---
const syntheticEvent = JSON.stringify([{
  fileDate: new Date().toISOString(),
  deviceTimestamp: Date.now(),
  userId: "e3359683-1ef2-4a0e-b57e-602aec57a0b1",
  referrerUrl: null,
  targetUrl: "https://mail.google.com/mail/u/0/#inbox",
  requestType: "GET",
  scheme: "https:",
  host: "mail.google.com",
  user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
  accept_language: "en-US,en;q=0.9",
  os_name: "macOS",
  browser_name: "Chrome",
  os_version: "10.15.7",
  browser_version: "130",
  timeZone: "America/New_York"
}]);

console.log("=== Blockify AES-GCM Key Extraction PoC ===");
console.log("Source: analytics.js:535 (hardcoded encryption_key)");
console.log("Key:    ej0hie1MThoo8Ri2 (16 bytes = AES-128-GCM)");
console.log("");

const encoded = encrypt(syntheticEvent);
console.log("Synthetic enRequest (as sent to /process):");
console.log(encoded);
console.log("");

const decoded = decrypt(encoded);
console.log("Decrypted payload:");
console.log(JSON.stringify(JSON.parse(decoded), null, 2));
console.log("");
console.log("Fields transmitted: userId (persistent UUID), targetUrl (visited URL),");
console.log("referrerUrl, os_name, browser_name, os_version, browser_version, timeZone.");

// If a captured base64 payload is passed as argument, also decrypt it
if (process.argv[2]) {
  console.log("\n=== Decrypting provided enRequest ===");
  try {
    const result = decrypt(process.argv[2]);
    console.log(JSON.stringify(JSON.parse(result), null, 2));
  } catch (e) {
    console.error("Decryption failed:", e.message);
  }
}
How to run it
  1. 1
    Save as blockify-decrypt.js.
  2. 2
    Run: node blockify-decrypt.js to confirm the key on a synthetic payload.
  3. 3
    For a real request: DevTools Network, filter insights.getblockify.com/process, copy enRequest, run: blockify-decrypt.js '<value>'
SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-829
SourceAI SANDBOX

Remote Blockify config can change active blocking behavior

We observed Blockify request https://blockify.b-cdn.net/switches190.json and apply the JSON as feature config.

It stores the response, enables/disables behaviors, and can apply blocking rules without a Store update.

01EvidenceCAUSE EFFECT
What actually happens
You did this

The extension starts its background service worker.

The same service worker also sets an hourly alarm for later refreshes.

The extension did this

The extension downloads remote feature switches and applies them to its blocking behavior.

The observed response populated the extension's stored switches.

02EvidenceNETWORK CAPTURE
Captured request
GEThttps://blockify.b-cdn.net/switches190.json
HTTP 200 JSON containing custom_dynamic_dnr, forced_exclusions, and feature toggles; GET request had no body.
03EvidenceFIELD TABLE
Remote configuration fields used by the extension
FieldValueWhy it matters
Dynamic blocking rules
[]This field can provide new request-blocking rules for the extension to add at runtime.
Forced exclusions
["nil"]This field can add sites that the extension allows through its blocking rules.
YouTube injection toggle
enabledThis decides whether the extension registers its YouTube blocking script in your browser.
CSS and script toggle
enabledThis controls whether the extension applies its CSS and JavaScript behavior on pages.
04EvidenceTEMPORAL PATTERN
When this fires
Every 1 hour

The extension fetches switches when the service worker loads, then schedules a refresh every 60 minutes.

05EvidenceCODE COMPARE
The code that does this

The service worker fetches and applies remote switches

What it actually does
Startup and hourly refresh call the remote-switch fetchbk_start.js
chrome.runtime.onStartup.addListener(() => {
  setupAlarm();
  updateYouTubeBlockingScript();
  //createContextMenu();
  //updateRules();

});

chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name == 'getSwitches') {
    setSwitches();
  }
});

setSwitches();
setupAlarm();
//check_opt_eli();
updateYouTubeBlockingScript();
//createContextMenu();
//updateRules();
Fetches switches190.json and stores the returned JSONbk_modules.js
async function setSwitches() {
  var url = 'https://blockify.b-cdn.net/switches190.json';

  fetch(url, { cache: 'no-cache' })
    .then(response => {
      // Check if the response is ok (status in the range 200-299)
      if (!response.ok) {
        console.error(response); //throw new Error(`HTTP error! Status: ${response.status}`);
      }
      // Parse the JSON data
      return response.json();
    })
    .then(data => {
      // Handle successful data retrieval
      if (data && data["spotify_injection"] && data["cssjs"]) {
        chrome.storage.local.set({ "switches": data });
        self.switches = JSON.parse(JSON.stringify(data));
        recheck_exclusions();
        cssjs_set();
        check_ruleset();
        updateYouTubeBlockingScript();
      }
    })
    .catch(error => {
      // Handle any errors that occurred during fetch
      console.error('Fetch error:', error.message);
      reportErrorToSentry(6, false, error, "bk_modules.js", "setSwitches");
    });
};
Applies server-provided dynamic request rulesbk_modules.js
function recheck_customDNR() {
  if (self.switches && self.switches["custom_dynamic_dnr"] && typeof self.switches["custom_dynamic_dnr"] == 'object' && self.switches["custom_dynamic_dnr"] != "" && self.switches["custom_dynamic_dnr"].length > 0) {
    //valid
    var rules = self.switches["custom_dynamic_dnr"];
    addRulesIfNotPresent(rules);
  }
};

async function addRulesIfNotPresent(rulesToAdd) {
  try {
    // 1. Retrieve the existing dynamic rules
    var existingRules = await chrome.declarativeNetRequest.getDynamicRules();
    var existingRuleIds = new Set(existingRules.map(rule => rule.id));

    // 2. Filter out any rules that have the same ID as an existing rule
    var filteredRules = rulesToAdd.filter(rule => !existingRuleIds.has(rule.id));

    // 3. If there are new, non-duplicate rules, add them
    if (filteredRules.length > 0) {
      await chrome.declarativeNetRequest.updateDynamicRules({ addRules: filteredRules });
    }
  }
  catch (err) {
    console.error('Error updating dynamic rules:', err);
  }
};
Creates the 60-minute refresh alarmbk_modules.js
async function setupAlarm() {
  var existingAlarm = await chrome.alarms.get('getSwitches');
  if (!existingAlarm) {
    // Create an alarm that fires every 60 minutes
    chrome.alarms.create('getSwitches', {
      periodInMinutes: 60
    }); // Repeats every 60 minutes
  }
};
06EvidenceTHIRD PARTY LIST
Remote configuration host
  • blockify.b-cdn.net

    Serves switches190.json, the configuration file that controls feature toggles and dynamic request rules.

+3 more findings not shown

Updated 30 September 2026nfmlkliedggdodlbgghmmchhgckjoaml