Is Ad Blocker: Stands AdBlocker safe?
Stands AdBlocker transmits every page URL and referrer to standsapp.org on each navigation, linked to a persistent user ID.
On every browser navigation, the extension batches the visited URL, previous URL, country code, and app version and POSTs them to thepromise-event.standsapp.org/convert. The country is resolved by sending the user's IP to prod.standsapp.org/geolookup at startup and stored alongside a server-assigned persistent user ID. CSS ad-blocking rules are fetched from static.standsapp.org and applied directly to page content; the fetch requests include the anonymous user ID and app version.
Who publishes itStands - no other listings under this identity
Stands - 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
Browsing History Sent to Remote Server on Every Page Visit
Dynamic analysis captured POSTs to thepromise-event.standsapp.org/convert with the URL of every page visited, referrer, a persistent anonymous ID, country, OS, version.
Events batch in tens.
Chrome enrolls by default; Firefox needs consent.
You navigate to any HTTP or HTTPS page.
The extension records the URL you visited, where you came from, and your transition type, then queues the row for transmission to standsapp.org.
No interaction required. Every completed navigation is captured, including searches, background tabs, and pages you close immediately.
| Content-Type | application/json |
| Cache-Control | no-cache |
{
"rows": [
{
"nid": "a141fe0b-3c2d-4f18-9e7a-0021deadbeef",
"pid": "",
"sid": "",
"cc": "GBR",
"ts": 1745001234567,
"rfu": "https%3A%2F%2Fwww.google.com%2F",
"tu": "https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FMain_Page",
"trt": "link",
"trq": "",
"os": "Windows",
"ver": "2.1.62",
"blk": 10
}
]
}| Field | Value | Why it matters | |
|---|---|---|---|
Page you visited | https://en.wikipedia.org/wiki/Main_Page | The exact URL of the page you just loaded, URL-encoded. | |
Page you came from | https://www.google.com/ | The URL of the previous page on that tab, or the URL of the tab that opened this one. | |
Your anonymous ID | a141fe0b-3c2d-4f18-9e7a-0021deadbeef | A UUID generated on first install and stored permanently. Links all your navigation records together across time. | |
Your country | GBR | Three-letter country code obtained by querying your IP address at startup. | |
How you got there | link | Whether you clicked a link, typed the URL, or were redirected, reveals your browsing patterns. | |
Your operating system | Windows | OS name, sent with every batch. | |
Extension version | 2.1.62 | Which version of Stands AdBlocker you have installed. | |
Timestamp | 1745001234567 | When the page finished loading, in milliseconds. |
The navigation listener and payload assembly from the extension's source:
// Fires whenever a tab navigates to a new URL.
// Captures the current URL and the previous URL on that tab,
// then queues a navigation record for transmission.
async function onTabUpdated(tabId, { url }) {
if (!url) return;
const previousPage = await pageData.get(tabId);
if (previousPage?.pageUrl === url) {
// Same URL — just refresh counters, don't re-report
await pageData.refreshBulk([tabId]);
return;
}
const [newPageData, tabState] = await Promise.all([
pageData.create(url),
tabContainer.get()
]);
// previousUrl = the page you were just on, or the tab that opened this one
newPageData.previousUrl =
previousPage?.pageUrl ||
tabState.tabOpenInitiators[tabId]?.url ||
'';
// 100 ms delay so transition metadata is available
setTimeout(async () => {
if (newPageData.isValidSite) {
await navigationReporter.queue([{
loadTime: Date.now(),
previousUrl: newPageData.previousUrl,
pageUrl: newPageData.pageUrl,
transitionType: tabState.transitions[tabId]?.[url]?.trt, // 'link' | 'typed' | ...
transitionQualifiers: tabState.transitions[tabId]?.[url]?.trq,
}]);
}
}, 100);
}// Collects up to 10 queued navigation rows, then POSTs them together.
async function sendNavigationBatch(navigationRows) {
const [anonymousId, operatingSystem, countryCode] = await Promise.all([
loadAnonymousId(), // persistent UUID from chrome.storage
getOperatingSystem(),
userData.getCountryCode() // obtained at startup from prod.standsapp.org/geolookup
]);
const payload = navigationRows.map(row => ({
nid: anonymousId, // your persistent identifier
pid: '',
sid: '',
cc: countryCode, // e.g. 'GBR'
ts: row.loadTime, // Unix ms timestamp
rfu: encodeURIComponent(row.previousUrl), // where you came from
tu: encodeURIComponent(row.pageUrl), // where you went
trt: row.transitionType || '', // 'link' | 'typed' | 'reload'
trq: row.transitionQualifiers?.join(',') || '',
os: operatingSystem,
ver: getExtensionVersion(),
blk: navigationRows.length
}));
await fetch('https://thepromise-event.standsapp.org/convert', {
method: 'POST',
body: JSON.stringify({ rows: payload })
});
}- thepromise-event.standsapp.org
Receives every navigation batch. Operated by Stands (the extension publisher). Subdomain name 'thepromise-event' does not appear in the Chrome Web Store listing.
- prod.standsapp.org
Primary API host. Used for user creation, geolookup, heartbeat, and error logging.
Function.prototype Globally Patched for Ad-Block Detection Evasion
The extension globally wraps Function.prototype.toString on sites where its ad-blocking scripts load, hiding proxied functions from detection.
On YouTube, injected code wraps Map.prototype.has and spoofs the player's user-agent.
You visit any website where the extension's proxy-apply.js script is loaded.
The extension replaces Function.prototype.toString with a Proxy, so wrapped functions return the original's source on .toString(), hiding the change from native-function checks.
On YouTube, a second mechanism injects code into the page via script-tag text replacement that additionally wraps Map.prototype.has and modifies the player's user-agent.
proxy-apply.js, Function.prototype.toString global patch:
// One-time setup: save the real toString, then replace it globally.
// The replacement walks a WeakMap to find the original function
// behind any chain of Proxy wrappers.
proxyApply.nativeToString = Function.prototype.toString;
const antiDetectionToString = new Proxy(Function.prototype.toString, {
apply(target, thisArg) {
// Follow the proxy chain back to the original function
let original = thisArg;
while (true) {
const unwrapped = proxyApply.proxies.get(original);
if (unwrapped === undefined) break;
original = unwrapped;
}
// Return what the native function would report
return proxyApply.nativeToString.call(original);
}
});
// Register the replacement itself in the WeakMap so it also looks native
proxyApply.proxies.set(antiDetectionToString, proxyApply.nativeToString);
// Install globally — affects all JS on the page
Function.prototype.toString = antiDetectionToString;youtube/isolated.js, Map.prototype.has wrap and ytcfg user-agent injection (YouTube-only):
// Wrap Map.prototype.has globally on YouTube to intercept the ad-blocker
// detection signal. YouTube's player code uses Map.has('onSnackbarMessage')
// as a sentinel during ad-blocking detection; wrapping it lets the extension
// observe and respond to that check without the page knowing.
window.Map.prototype.has = new Proxy(window.Map.prototype.has, {
apply(originalHas, mapInstance, [key]) {
if (key === 'onSnackbarMessage' && !adBlockerDetectionHandled) {
const player = document.getElementById('movie_player');
if (player) {
const stats = player.getStatsForNerds?.();
const buffering = player.getPlayerStateObject?.()?.isBuffering;
const trackingUrl = player.getPlayerResponse?.()?.playbackTracking
?.videostatsPlaybackUrl?.baseUrl;
// If buffered to 0x0 resolution while supposedly buffering → detection event
if (buffering && stats?.buffer_health_seconds === '0.00 s' &&
stats?.resolution === '0x0' && retryTokens.length > 0) {
if (trackingUrl?.includes('reloadxhr')) retryTokens.shift();
adBlockerDetectionHandled = true;
}
}
}
return Reflect.apply(originalHas, mapInstance, [key]);
}
});// Modify the YouTube player's user-agent string to append one of several
// device-context tokens. The player sends this string to YouTube's ad-decision
// API via INNERTUBE requests; the extension rotates tokens to bypass
// ad-block detection in the ad-serving pipeline.
function setUserAgentToken(token) {
const baseUA = ytcfg.data_.INNERTUBE_CONTEXT.client.userAgent;
ytcfg.data_.INNERTUBE_CONTEXT.client.userAgent = token
? baseUA.replace(/(Mozilla\/5\.0 \([^)]+)/, `$1; ${token}`)
: baseUA; // restore original when token is null
}
// Tokens tried in order: 'channel', then 'adunit', 'lactmilli', 'instream', 'eafg'
const retryTokens = ['channel'];
setUserAgentToken(retryTokens[0]);Replacing `Function.prototype.toString` affects every script running on the page, including the website's own code and other extensions. The patch is benign in intent — it prevents ad-blocker detection scripts from identifying wrapped functions — but it establishes a pattern where the extension modifies shared JavaScript primitives outside its own scope. The YouTube-specific patches (`Map.prototype.has`, `ytcfg`, `Promise.prototype.then`) are injected via script-tag text replacement in the isolated world, which means they run in the page's MAIN context without a `world: 'MAIN'` declaration on the script registration.
Extension Interactions and URLs Reported to Vendor Server
The extension sends records of blocking toggles, allowlist changes, and filtered sites to prod.standsapp.org/api/v2/events.
Each POST carries a server-assigned user ID, a local anonymous ID, version, and your other extensions.
You enable or disable the extension on a specific website, add or remove a site from your allowlist, or open an extension-internal page.
The extension queues an event with the affected URL and a numeric event type, then POSTs it with your persistent user identifiers once 10 events accumulate.
When the 'management' permission is granted, the POST also includes the IDs of every other extension installed in your browser.
| Content-Type | application/json |
{
"privateUserId": "b1c9f3a2-d45e-4812-a678-1234567890ab",
"anonymousUserId": "a141fe0b-3c2d-4f18-9e7a-0021deadbeef",
"installedExtensions": [],
"appVersion": "2.1.64",
"extensionId": "lgblnfidahcdcjddiepkckcfdhpknnjh",
"events": [
{
"eventType": 6,
"state": "disabled",
"url": "https://www.example.com/account/",
"eventTime": "2025-04-29T10:15:00.000Z"
}
]
}| Field | Value | Why it matters | |
|---|---|---|---|
Server-assigned user ID | b1c9f3a2-d45e-4812-a678-1234567890ab | A persistent ID the vendor's server assigns on first install and stores locally. Present in every POST to link all your event logs together. | |
Anonymous device ID | a141fe0b-3c2d-4f18-9e7a-0021deadbeef | A UUID generated on your device at install and stored permanently. Identifies your device independently of the server-assigned ID. | |
Affected site URL | https://www.example.com/account/ | The URL of the site where the action occurred. Present in state-change, allowlist, filtering, and extension-page-open events. | |
Other installed extension IDs | cjpalhdlnbpafiamejdnhcphjbkeiagm, uBlock0@raymondhill.net | Chrome extension IDs of every other extension in your browser, included when the optional 'management' permission is granted. | |
Extension version | 2.1.64 | Which version of Stands AdBlocker sent this log entry. |
ServerLogger.sendToServer, payload assembly and delivery:
// Collects persistent user identifiers, optionally the list of installed
// extensions, then POSTs up to 10 queued events to the vendor logging endpoint.
async function sendEventsToServer(events) {
const user = await getUserData();
const anonymousId = await loadAnonymousId();
const hasManagement = await hasPermission('management');
let otherExtensions = [];
if (hasManagement) {
otherExtensions = await chrome.management.getAll();
}
const payload = {
privateUserId: user?.privateUserId, // server-assigned persistent ID
anonymousUserId: anonymousId, // locally-generated UUID
installedExtensions: otherExtensions.map(e => e.id),
appVersion: getExtensionVersion(),
extensionId: getExtensionId(),
events, // array of up to 10 event objects
};
await fetch('https://prod.standsapp.org/api/v2/events', {
method: 'POST',
body: JSON.stringify(payload),
});
}// Event types that carry the affected site URL in plaintext:
// eventType 6 — user toggled the blocker on or off for this site
function onStateChange(enabled, url) {
return { eventType: 6, state: enabled ? 'enabled' : 'disabled', url };
}
// eventType 8 — user modified the allowlist for this site
function onAllowlistChange(url, type, added) {
return { eventType: 8, type, state: added ? 'added' : 'removed', url };
}
// eventType 9 — user opened an extension-internal URL
function onExtensionPageOpen(url) {
return { eventType: 9, url };
}
// eventType 15 — content filtering ran on this URL
function onUrlFiltered(type, url) {
return { eventType: 15, type, bUrl: url };
}- prod.standsapp.org
Primary vendor API, receives event logs at /api/v2/events. Also used for user account creation, geolookup (/geolookup), and heartbeat pings.
Where it sends data
Destinations our analysis observed Stands AdBlocker 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.
- prod.standsapp.org
Stands AdBlocker sends data to prod.standsapp.org. One other extension we have analysed sends data here.
- thepromise-event.standsapp.org
Stands AdBlocker sends data to thepromise-event.standsapp.org. One other extension we have analysed sends data here.
- static.standsapp.org
Stands AdBlocker sends data to static.standsapp.org. No other extension we have analysed sends data here.