Is UltraBlock - Adblock & Privacy Protection safe?
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.
Who publishes itNinble - no other listings under this identity
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.
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
You finish loading any http or https page.
This includes ordinary browsing such as a product search on a shopping site.
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.
- Page protocolhttps:
The http or https scheme of the page you're on.
- Site domainwww.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 ID1f3a9c7e2b4d4a1f8e6c0d9b2a3f5e71 (illustrative)
A persistent ID generated once and reused on every request, letting UltraBlock link your visits across sessions.
- Cookie count0
How many cookies UltraBlock has recorded for this site.
Building and sending the per-page request
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 }));JSON list of matching third-party services for the hostname, used to drive the extension's on-page blocking.
- Content-Type
- application/x-www-form-urlencoded
- UltraBlock-UDID
- 1f3a9c7e2b4d4a1f8e6c0d9b2a3f5e71 (illustrative)
- UltraBlock-Version
- 1.7.1
source=extension&request=getServices&protocol=https:&hostname=www.amazon.com&pathname=/s&search=?k=usb+cable&hostnames=1&services=1&frames=0&cookies=0- 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
You enable the sharing option in UltraBlock's settings page.
The visible label says "Anonymously share results with UltraBlock."
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.
- Cookie names and valuescookieName=session_id; cookieValue=s%3A2d7f65b8aa8c46f5
Cookie values can identify your browser session or account-specific state for a site you visited.
- Extension device identifier1f3a9c04d7b58e29a60c91ef42a73b5d
A stable identifier lets multiple uploaded summaries be tied back to the same browser installation.
- Visited page URLhttps://news.example.test/article?id=4821
The URL shows which page was summarized when the extension flushed its stored data.
- Hostnames contacted on the pagecdn.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 recordspages=1; services=2; frames=1; rows=5
These records combine page counts, blocked or observed services, frames, and timestamps into one browsing summary.
The shipped code collects cookies, encrypts the summary, and posts it to UltraBlock
Default settings include a hardcoded encryption key
js/backend/background.jsfunction _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.jsfunction _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.jsfunction _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 bundled AES helper produces the encrypted data field
AES.enc and AES.dec derive key material from the bundled key
js/backend/aes.jsenc = 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);};- 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
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.