Is UltraBlock - Adblock & Privacy Protection safe?

High risk

UltraBlock sends each visited page URL and a persistent device ID to api.ultrablock.org as tabs finish loading.

When a page finishes loading, UltraBlock posts the page protocol, hostname, path, query string, and local service/frame/cookie counts with its persistent UltraBlock UDID. If anonymous sharing is enabled, it also encrypts a browsing summary that can include page URLs, hostnames, frames, service lists, and collected cookie names and values, then sends it to the same API.

Ninblev1.7.1Chrome Web Store
78Risk
Who publishes it

Ninble - 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
Ninble
Registered address
De Savornin lohmanlaan 31, Groningen 9722 HC, NL
Registered contact
Christian Branbergen

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

Publishers can request a review.

Findings

UltraBlock sends the full URL of every page you visit to api.ultrablock.org

UltraBlock's background script posts the full URL of every page you load, including query strings, to api.ultrablock.org with a persistent device ID.

Planted canary search terms round-tripped verbatim into the outbound body.

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

You finish loading any http or https page.

This includes ordinary browsing such as a product search on a shopping site.

The extension did this

UltraBlock's background service worker sends the page's full URL and a persistent device ID to its own server.

The request fires within about a second of the page finishing load, on every site, not just ones you asked it to check.

What's in the outbound request
  • Page protocol
    https:

    The http or https scheme of the page you're on.

  • Site domain
    www.amazon.com

    The website's domain, sent on every page load.

  • Page path
    /s

    The exact path within the site you're viewing.

  • Query string
    ?k=usb+cable

    The full query parameters from the URL, including anything you searched for.

  • Device ID
    1f3a9c7e2b4d4a1f8e6c0d9b2a3f5e71 (illustrative)

    A persistent ID generated once and reused on every request, letting UltraBlock link your visits across sessions.

  • Cookie count
    0

    How many cookies UltraBlock has recorded for this site.

The code that does this

Building and sending the per-page request

Readable version

Navigation listener schedules the call

setTimeout(function () {  _getServicesFromAPI(tab.url);  if (samePageStillLoaded(tabSummary.hostname, i, tab.url)) {    markPageChecked(tabSummary.hostname, i);  }}, 1000);

Assembling and posting the request body

const headers = new Headers();headers.append('Content-Type', 'application/x-www-form-urlencoded');headers.append('UltraBlock-UDID', settings.udid);headers.append('UltraBlock-Version', settings.version);const body =  'source=extension&request=getServices' +  '&protocol=' + encodeURIComponent(protocol) +  '&hostname=' + encodeURIComponent(hostname) +  '&pathname=' + encodeURIComponent(pathname) +  '&search=' + encodeURIComponent(search) +  '&hostnames=' + hostnamesCount +  '&services=' + servicesCount +  '&frames=' + framesCount +  '&cookies=' + cookiesCount;fetch(new Request('https://api.ultrablock.org/', { method: 'POST', headers, body }));
Captured request
POSThttps://api.ultrablock.org/

JSON list of matching third-party services for the hostname, used to drive the extension's on-page blocking.

Headers
Content-Type
application/x-www-form-urlencoded
UltraBlock-UDID
1f3a9c7e2b4d4a1f8e6c0d9b2a3f5e71 (illustrative)
UltraBlock-Version
1.7.1
Body
source=extension&request=getServices&protocol=https:&hostname=www.amazon.com&pathname=/s&search=?k=usb+cable&hostnames=1&services=1&frames=0&cookies=0
Where the data goes
    • api.ultrablock.org

    Receives the full URL (protocol, domain, path, query string) of every page you visit plus a persistent device ID. Operated by UltraBlock's publisher, Ninble.

Opt-in sharing uploads cookies to UltraBlock

When you enable "Anonymously share results with UltraBlock," the extension records cookie changes, builds a summary, encrypts it with a bundled key, and POSTs it to api.ultrablock.org with URLs, hostnames, device ID, and cookie values.

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

You enable the sharing option in UltraBlock's settings page.

The visible label says "Anonymously share results with UltraBlock."

The extension did this

The extension uploads an encrypted browsing summary that can include cookie names and values.

The upload happens when the extension flushes stored data for a hostname that is no longer active.

Fields placed into the uploaded summary object
  • Cookie names and values
    cookieName=session_id; cookieValue=s%3A2d7f65b8aa8c46f5

    Cookie values can identify your browser session or account-specific state for a site you visited.

  • Extension device identifier
    1f3a9c04d7b58e29a60c91ef42a73b5d

    A stable identifier lets multiple uploaded summaries be tied back to the same browser installation.

  • Visited page URL
    https://news.example.test/article?id=4821

    The URL shows which page was summarized when the extension flushed its stored data.

  • Hostnames contacted on the page
    cdn.example.test, analytics.example.test

    This adds a map of third-party hosts and services seen while you used the page.

  • Page, service, and frame records
    pages=1; services=2; frames=1; rows=5

    These records combine page counts, blocked or observed services, frames, and timestamps into one browsing summary.

Captured request
POSThttps://api.ultrablock.org/
The code that does this

The shipped code collects cookies, encrypts the summary, and posts it to UltraBlock

Readable version

Default settings include a hardcoded encryption key

js/backend/background.js
function _createDefaultSettings() {  try {    const manifest = _getBrowser().runtime.getManifest();    _settings = {      udid: _generateRandomUDID(),      version: manifest.version,      encryptionKey: "b8e0950f25d18e72",      clearCookieDomains: [],      clearCacheSince: false,      runningSince: 0,      deleteCookies: false,      sendDNT: true,      clearBrowserCache: true,      randomizeUserAgent: false,      disableUltraBlock: false,      shareData: false,      lastDatabaseUpdate: "2017-01-01",      knownServices: [],      knownUserAgents: [],      knownWhitelistedWebsites: [],      userDisallowedHostnames: []    };    console.log("%cDefault settings are restored.", "color: grey;");    _saveSettings();  } catch (error) {    console.log("%cError creating default settings", "color: red;");  }}

Cookie changes become summary rows

js/backend/background.js
_getBrowser().cookies.onChanged.addListener(function(e) {  try {    if (!_activeTab || !_activeTab.id) return false;    const url = _activeTab.url;    const hostname = uri.getHostname(_activeTab.url);    const domain = uri.getDomain(hostname);    if (uri.validateHostname(hostname) == true) {      let cookieDomain = e.cookie.domain;      if (cookieDomain.startsWith(".") == true) cookieDomain = cookieDomain.substr(1);      let thirdPartyCookie = false;      if (cookieDomain != domain) thirdPartyCookie = true;      let domainFound = false;      if (_hostnames[hostname] != undefined) {        for (let i = 0; i < _hostnames[hostname].length; i++) {          const object = _hostnames[hostname][i];          if (object.domain == cookieDomain) {            domainFound = true;            break;          }        }      } else {        console.log("%cDropped the _hostname ball for " + hostname + ". (needs to be fixed).", "color: grey;");      }      if (thirdPartyCookie == false || thirdPartyCookie == true && domainFound == true) {        _insertCookieObject(url, hostname, cookieDomain, e.cookie.domain, e.cookie.name, e.cookie.value, e.cookie.secure, thirdPartyCookie);      }    }  } catch (error) {    console.log("%cError cookies changing", "color: red;");  }});

Sharing sends an encrypted setSummaryFile request

js/backend/background.js
function _flushHostnames() {  try {    console.log("%cChecking if we need to do some flushing.", "color: grey;");    const activeTabHostnames = [];    for (const tabId in _tabs) {      const object = _tabs[tabId];      if (object && object.hostname) activeTabHostnames.push(object.hostname);    }    if (_validateUDID() == false) _updateUDID();    for (const hostname in _hostnames) {      if (activeTabHostnames.indexOf(hostname) == -1 && hostname != "") {        console.log("%cFlushing " + hostname + " (still " + activeTabHostnames.length + " active hostnames).", "color: green;");        const manifest = _getBrowser().runtime.getManifest();        if (_pages[hostname] == undefined) _pages[hostname] = [];        if (_services[hostname] == undefined) _services[hostname] = [];        if (_hostnames[hostname] == undefined) _hostnames[hostname] = [];        if (_frames[hostname] == undefined) _frames[hostname] = [];        if (_cookies[hostname] == undefined) _cookies[hostname] = [];        const rows = _pages[hostname].length + _services[hostname].length + _hostnames[hostname].length + _frames[hostname].length + _cookies[hostname].length;        if (rows > 0) {          const summary = {            source: "extension",            udid: _settings.udid,            version: manifest.version,            encryptionKey: _settings.encryptionKey,            url: _pages[hostname][0].url,            hostname: hostname,            domain: uri.getDomain(hostname),            pages: _pages[hostname],            services: _services[hostname],            hostnames: _hostnames[hostname],            frames: _frames[hostname],            cookies: _cookies[hostname],            rows: rows,            timestamp: _getTimestamp()          };          if (_settings.shareData == true) {            const data = JSON.stringify(summary);            const encryptedData = AES.enc(data, _settings.encryptionKey);            const dataLength = data.length;            const headers = new Headers();            headers.append("Content-Type", "application/x-www-form-urlencoded");            headers.append("UltraBlock-UDID", _settings.udid);            headers.append("UltraBlock-Version", _settings.version);            const requestUrl = "https://api.ultrablock.org/";            const requestBody = "source=extension&request=setSummaryFile&data=" + encodeURIComponent(encryptedData);            const request = new Request(requestUrl, { method: "POST", headers: headers, body: requestBody });            fetch(request).then(response => {              return response.json();            }).then(responseData => {              console.log("%cConnecting to UltraBlock API to send summary file.", "color: green;");              if (responseData.result == true) {                if (responseData.decryption == true) console.log("%cFlushed data received and decrypted by UltraBlock API contained in " + responseData.size + " bytes.", "color: green;");                else console.log("%cError: Decryption failed on server side.", "color: red;");              } else {                console.log("%cFailed to obtain response confirmation from UltraBlock API.", "color: red;");              }              console.log("%cFlushed data sent to UltraBlock API " + dataLength + " bytes.", "color: green;");            }).catch(error => {              console.log("%cError flushing data to UltraBlock API: " + error.message, "color: red;");              console.log(error);              console.log("%cAttempted to send " + dataLength + " bytes to " + requestUrl, "color: grey;");            });          } else {            console.log("%cSharing results is disabled.", "color: grey;");          }        }        if (_settings.disableUltraBlock == false && _settings.deleteCookies == true) {          for (let i = 0; i < _cookies[hostname].length; ++i) {            const object = _cookies[hostname][i];            const domain = uri.getDomain(object.cookieDomain);            if (object.thirdPartyCookie == true && _settings.clearCookieDomains.indexOf(domain) == -1) _settings.clearCookieDomains.push(domain);          }          _saveSettings();          _startCookieManager();        }        _clearBrowserCache();        delete _pages[hostname];        delete _hostnames[hostname];        delete _services[hostname];        delete _frames[hostname];        delete _cookies[hostname];      }    }  } catch (error) {    console.log("%cError flushing hostnames", "color: red;");  }}

The settings checkbox controls shareData

js/frontend/pages/settings.js
function _updateSettings() {  setTimeout(() => {    const deleteCookies = _getElement("delete-cookies-checkbox");    const sendDnt = _getElement("send-dnt-checkbox");    const clearBrowserCache = _getElement("clear-browser-cache-checkbox");    const randomizeUserAgent = _getElement("randomize-user-agent-checkbox");    const shareData = _getElement("share-data-checkbox");    if (!_portSettings) _initializeConnection();    _portSettings.postMessage({      target: "ultrablock.background.js",      option: "updateSettingsFromSettings",      deleteCookies: deleteCookies.checked,      sendDNT: sendDnt.checked,      clearBrowserCache: clearBrowserCache.checked,      randomizeUserAgent: randomizeUserAgent.checked,      shareData: shareData.checked    });    console.log("%cUpdating settings via background script.", "color: green;");  }, 500);}
The code that does this

The bundled AES helper produces the encrypted data field

Readable version

AES.enc and AES.dec derive key material from the bundled key

js/backend/aes.js
enc = function(string, pass, binary) {  const salt = randArr(8);  const pbe = openSSLKey(s2a(pass, binary), salt);  const key = pbe.key;  const iv = pbe.iv;  const saltBlock = [[83, 97, 108, 116, 101, 100, 95, 95].concat(salt)];  string = s2a(string, binary);  const cipherBlocks = rawEncrypt(string, key, iv);  return Base64.encode(saltBlock.concat(cipherBlocks));};dec = function(string, pass, binary) {  let cryptArr = Base64.decode(string);  const salt = cryptArr.slice(8, 16);  const pbe = openSSLKey(s2a(pass, binary), salt);  const key = pbe.key;  const iv = pbe.iv;  cryptArr = cryptArr.slice(16, cryptArr.length);  return rawDecrypt(cryptArr, key, iv, binary);};
Destination named by the upload path
    • api.ultrablock.org

    Receives the encrypted setSummaryFile summary when the sharing setting is enabled.

What it can do

Permissions this extension asks for, as declared in version 1.7.1. 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>

  • Read and change cookies, including the ones that keep you signed in

    cookies

  • Store data in your browser

    storage

  • Watch every request your browser makes

    webRequest

  • See the address and title of every tab you have open

    tabs

  • Clear your browsing history, cache and cookies

    browsingData

  • Act on the current tab, but only after you click the extension

    activeTab

  • Block and redirect the requests your browser makes

    declarativeNetRequest

  • See which of your requests its blocking rules matched

    declarativeNetRequestFeedback

declarativeNetRequestWithHostAccess

Where it sends data

Destinations our analysis observed UltraBlock 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.

  • api.ultrablock.org

    UltraBlock sends data to api.ultrablock.org. One other extension we have analysed sends data here.

Updated 30 September 2026ijnodnmgpgmgaajhagolfiemmmamfcgb