Is Free Adblocker for Chrome by Open Adblocker safe?

Medium risk

Open Adblocker is medium risk. The source includes built-in exceptions for Urban VPN analytics/stats hosts, loaded as trusted custom filters at startup. These let matching Urban VPN requests pass even when a user-defined blocking rule would otherwise match the same host.…

openadblockerdeveloperv1.6.13Chrome Web Store
49Risk

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

Publishers can request a review.

Findings

Urban VPN analytics rules are trusted by default

The source includes built-in exceptions for Urban VPN analytics/stats hosts, loaded as trusted custom filters at startup.

These let matching Urban VPN requests pass even when a user-defined blocking rule would otherwise match the same host.

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

You install or run the ad-blocking extension.

The extension did this

The extension loads built-in exception rules for Urban VPN analytics and stats hosts.

Those rules are configured as a trusted custom filter before filtering begins.

Built-in exception rules that affect Urban VPN requests
  • Stats host exception
    @@||stats.urban-vpn.com^$important

    Requests to this stats host can be allowed even when a blocking rule would otherwise match.

  • Analytics host exception
    @@||analytics.urban-vpn.com^$important

    Requests to this analytics host can be allowed even when a blocking rule would otherwise match.

  • Urban VPN cid URLs
    @@||urban-vpn.com^*^cid=$document,subdocument,important

    Urban VPN document and frame URLs containing a cid parameter can be exempted from blocking.

The code that does this

The shipped service worker hardcodes and trusts the Urban VPN exceptions

Readable version

Built-in custom filter rules

service_worker.js
const CUSTOM_FILTER_ID = 1001;const CUSTOM_FILTER_RULES = [  "@@||openadblocker.com^$important",  "@@||stats.urban-vpn.com^$important",  "@@||analytics.urban-vpn.com^$important",  "@@||urban-vpn.com^*^cid=$document,subdocument,important",];

Default configuration marks the custom filter trusted

service_worker.js
const DEFAULT_EXTENSION_CONFIG = () => {  const customRules = new es_FilterList(CUSTOM_FILTER_RULES.join("\n"));  return {    staticFiltersIds: [],    customFilters: [      {        filterId: CUSTOM_FILTER_ID,        content: customRules.getContent(),        trusted: true,        conversionData: customRules.getConversionData(),      },    ],    trustedDomains: [],    allowlist: [],    userrules: {      content: userRulesFilter.getContent(),      conversionData: userRulesFilter.getConversionData(),    },    verbose: true,    filtersPath: "filters",    ruleSetsPath: "filters/declarative",    declarativeLogEnabled: true,    settings: {      assistantUrl: "assistant-inject.js",      gpcScriptUrl: "gpc.js",      hideDocumentReferrerScriptUrl: "hide-document-referrer.js",      collectStats: true,      allowlistEnabled: true,      allowlistInverted: false,      stealthModeEnabled: true,      filteringEnabled: true,      debugScriptlets: false,      stealth: {        blockChromeClientData: false,        hideReferrer: false,        hideSearchQueries: false,        sendDoNotTrack: false,        blockWebRTC: false,        selfDestructThirdPartyCookies: false,        selfDestructThirdPartyCookiesTime: 3600,        selfDestructFirstPartyCookies: false,        selfDestructFirstPartyCookiesTime: 3600,      },    },  };};

Startup path uses that default configuration

service_worker.js
const setupAdGuard = () => {  TsWebExtension.setLocalScriptRules(localScriptRules);  adGuardSetupAsync();};const adGuardSetupAsync = async () => {  const tsWebExtension = new TsWebExtension("/web-accessible-resources");  await tsWebExtension.initStorage();  const config = await getConfiguration();  await tsWebExtension.start(config);  setUpTsWebExtensionMessages(tsWebExtension);  adguard_setup_injections.push({ key: AdGuardIdentifiers._config, use: config, value: true });  adguard_setup_injections.push({ key: AdGuardIdentifiers._tsWebExtension, use: tsWebExtension, value: true });  inject(adguard_setup_injections);  dispatcher().onWithClass(FiltersStateChangedListener);  dispatcher().onWithClass(WebRTCStateChangedListener);  dispatcher().onWithClass(RulesUpdatedListener);  dispatcher().onWithClass(WhitelistUpdatedListener);  const readyMessage = { type: AdGuardMessages.ready, force: true };  await dispatcher().sendMessage(readyMessage);};const getConfiguration = async () => {  const config = DEFAULT_EXTENSION_CONFIG();  config.allowlist = getDomainsWithSubDomains(await whiteList().getDomains());  config.staticFiltersIds = await useInternalFilters().getEnabledFilters();  config.settings.stealth.blockWebRTC = await useInternalWebRTC().getState();  const userRules = new es_FilterList((await useInternalManualBlocking().getUserRules()).join("\n"));  config.userrules = {    content: userRules.getContent(),    conversionData: userRules.getConversionData(),  };  return config;};const setUpTsWebExtensionMessages = (tsWebExtension) => {  const handler = tsWebExtension.getMessageHandler();  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {    handler(message, sender).then((response) => sendResponse(response));    return true;  });};
External hosts named by the trusted exception rules
    • stats.urban-vpn.com

    Urban VPN stats host protected by a built-in important exception rule.

    • analytics.urban-vpn.com

    Urban VPN analytics host protected by a built-in important exception rule.

    • urban-vpn.com

    Urban VPN document and frame URLs with a cid parameter are covered by the built-in exception rule.

Adblocker toggle sends active tab domain to analytics

Free Adblocker sends interaction analytics to analytics.openadblocker.com.

A captured request had a panelist ID, a CLICK action, and SETTINGS->OPEN_SETTINGS details; code adds the tab's domain on toggle clicks, unlike the store listing.

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

You click a control in the extension popup.

The adblocker toggle path is the one that adds the active tab's domain.

The extension did this

The extension sends an analytics event with a persistent identifier and interaction details.

For toggle clicks, the service worker adds the current tab hostname before posting the event.

Captured request
POSThttps://analytics.openadblocker.com/rest/v1/analytics/event

Observed during dynamic analysis with panelistId, eventAction CLICK, and eventDetails SETTINGS->OPEN_SETTINGS; that run did not capture a domain-bearing toggle body.

Headers
Content-Type
application/json
Analytics event fields built before the POST
  • Persistent panelist ID
    A7fQp9Lm2ZxT4cB8nR0sYu

    Lets multiple extension interactions be tied to the same browser profile over time.

  • Active tab domain
    developer.chrome.com

    Shows which website was open when you used the adblocker toggle.

  • Clicked control
    ADBLOCKER_TOGGLE

    Shows which extension UI element you used.

  • Action target
    PAUSE

    Shows what the extension control was trying to do at the time of the event.

  • Extension version
    1.6.5

    Adds software-version context to the interaction record.

  • Event timestamp
    2026-07-12T22:48:38.676Z

    Shows when the interaction happened.

The code that does this

Popup click to analytics POST path

Readable version

Popup toggle click sends an adblocker_toggle user-activity event

popup/popup.js
const useExternalAdBlocker = () => {  if (!la) throw new Error('AdBlocker is not set up. Please call "setupExternalAdBlocker" first.');  return la},da = {  class: "ad-blocker-toggle",  "data-test": "adblocker-toggle"},ua = defineComponent({  __name: "toggle",  setup(e) {    const n = ref(!1),      o = Wi(),      s = useUserActivity(),      d = useExternalAdBlocker(),      u = runtime_core_esm_bundler_computed(() => o.app.isServicePage ? "Can’t block on this page" : o.app.isPaused ? "Resume for this website" : "Pause for this website"),      onChange = async () => {        if (o.app.isServicePage || n.value) return;        n.value = !0;        const e = o.app.isPaused;        await s.click(Bi.adblockerToggle, {          to: e ? Pi.play : Pi.pause,          page: yo.HOME        });        try {          await d.toggle(!e), n.value = !1        } catch {          n.value = !1        }      };    return (e, s) => (openBlock(), createElementBlock("div", da, [On(Zi, {      class: "ad-blocker-toggle__button",      loading: n.value,      onClick: onChange    }, null, 8, ["loading"]), createBaseVNode("div", {      class: normalizeClass([{        "ad-blocker-toggle__title--disabled": unref(o).app.isServicePage      }, "ad-blocker-toggle__title"]),      "data-test": "adblocker-toggle__title"    }, toDisplayString(u.value), 3)]))  }});

AnalyticsService builds the JSON body and POSTs it with credentials included

service_worker.js
let AnalyticsService = class AnalyticsService {  options;  identifier;  constructor($, z) {    this.options = $, this.identifier = z  }  async track({    manual: $,    action: z,    details: _t  }) {    const Et = {        clientVersion: this.options.version,        eventSource: this.options.source,        mode: (await chrome.management.getSelf()).installType,        timestamp: Date.now(),        panelistId: await this.identifier.getUserId(),        manualEvent: $,        eventDetails: this.upperCase(_t),        eventAction: z      },      Lt = `${this.options.api}/analytics/event`;    await fetch(Lt, {      body: JSON.stringify(Et),      cache: "no-cache",      credentials: "include",      headers: {        "Content-Type": "application/json"      },      method: "POST"    })  }  upperCase($) {    const z = {};    for (const _t in $) Object.assign(z, {      [_t.toUpperCase()]: $[_t]    });    return z  }};

Click-event strategy copies payload.domain into event details

service_worker.js
const EVENT_STRATEGY = {  [AnalyticsAction.click]: $ => {    const {      sessionId: z,      element: _t,      payload: Et    } = $, Lt = ClickElementLinks.includes(_t);    return {      action: AnalyticsAction.click,      manual: !0,      details: {        toolbar_session_id: z,        id: _t.toUpperCase(),        source: Et.page,        to: Lt ? Et.to : Et.to.toUpperCase(),        type: Lt ? ClickElementType.link : ClickElementType.button,        ..."domain" in Et && {          domain: Et.domain        }      }    }  },  [AnalyticsAction.extensionEnabled]: () => ({    action: AnalyticsAction.extensionEnabled,    manual: !0,    details: {      source: "SYSTEM"    }  }),  [AnalyticsAction.pageShown]: $ => {    const {      page: z,      sessionId: _t    } = $;    return {      action: AnalyticsAction.pageShown,      manual: !1,      details: {        id: z,        toolbar_session_id: _t      }    }  },  [AnalyticsAction.toggle]: $ => {    const {      element: z,      sessionId: _t,      action: Et    } = $;    return {      action: AnalyticsAction.toggle,      manual: !0,      details: {        toggle_id: z.toUpperCase(),        toolbar_session_id: _t,        action: Et ? ToggleAction.on : ToggleAction.off      }    }  },  [AnalyticsAction.settingsImportError]: $ => {    const {      reason: z    } = $;    return {      action: AnalyticsAction.settingsImportError,      manual: !0,      details: {        reason: z      }    }  },  [AnalyticsAction.snackbarShown]: $ => {    const {      snackId: z,      snackSuccess: _t    } = $;    return {      action: AnalyticsAction.snackbarShown,      manual: !0,      details: {        snack_id: z.toUpperCase(),        snack_success: _t      }    }  }};

Service worker adds active-tab domain before tracking adblocker-toggle clicks

service_worker.js
analytics_setup_injections = [{  key: AnalyticsIdentifiers.options,  use: {    api: "https://analytics.openadblocker.com/rest/v1",    source: "OPEN_AD_BLOCKER",    version: chrome.runtime.getManifest().version  },  value: !0}, {  key: AnalyticsIdentifiers.service,  use: AnalyticsService}], setupInternalAnalytics = () => {  chrome.management.onEnabled.addListener($ => {    const z = {      type: AnalyticsMessages.extensionEnabled,      payload: $    };    dispatcher().sendMessage(z)  }), chrome.permissions.onAdded.addListener(on_permissions_added_handler_onPermissionsAddedHandler), chrome.permissions.onRemoved.addListener(onPermissionsRemovedHandler), onIdentifierReady(handleIdentifierReady)}, handleIdentifierReady = async () => {  inject(analytics_setup_injections), dispatcher().onWithClass(ExtensionEnabledListener), onUserActivity(analytics_setup_handleUserActivity)}, analytics_setup_handleUserActivity = async ({  message: $}) => {  const z = {      [UserActivityType.click]: async $ => {        const z = $;        if (z.element === ElementsUI.adblockerToggle) {          const $ = await getActiveTabHelper();          $?.url && Object.assign(z.payload, {            domain: getDomainHelper($.url)          })        }        const _t = buildEvent(AnalyticsAction.click, z);        await utils_analytics().track(_t)      },      [UserActivityType.visitPage]: async $ => {        const z = buildEvent(AnalyticsAction.pageShown, $);        await utils_analytics().track(z)      },      [UserActivityType.toggle]: async $ => {        const z = buildEvent(AnalyticsAction.toggle, $);        await utils_analytics().track(z)      },      [UserActivityType.settingsImportError]: async $ => {        const z = buildEvent(AnalyticsAction.settingsImportError, $);        await utils_analytics().track(z)      },      [UserActivityType.snackbarShown]: async $ => {        const z = buildEvent(AnalyticsAction.snackbarShown, $);        await utils_analytics().track(z)      }    },    {      type: _t    } = $.payload;  z[_t]?.($.payload)};

Persistent panelist ID is stored in chrome.storage.local

service_worker.js
let IdentifierService = class IdentifierService {  storage = makeDataAccessor("local", "USER_ID", {    useCache: !1  });  async getUserId() {    if (await this.storage.exists()) return await this.storage.read();    const $ = this.generateKey();    return await this.storage.write($), $  }  generateKey() {    let $ = "";    const z = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";    for (let _t = 0; _t < 22; _t++) $ += z.charAt(Math.floor(62 * Math.random()));    return logger.info("User key generated:", $), $  }};
Analytics destination
    • analytics.openadblocker.com

    Receives extension interaction analytics, including the persistent panelist identifier and event details built by the service worker.

Updated 30 September 2026jncabfnemmiofhiimdgelgeobggelpci