Is Ad Blocker: Stands AdBlocker safe?

High risk

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.

Standsv2.1.71Chrome Web Store
75Risk
Who publishes it

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.

Publisher
Stands
Declared legal entity
Stands
Registered address
Yosef Karo 7, Suit 22, Tel Aviv 6701407, Israel
Registered contact
Roy Rosenfeld

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

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You navigate to any HTTP or HTTPS page.

The extension did this

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.

02EvidenceNETWORK CAPTURE
Captured request
POSThttps://thepromise-event.standsapp.org/convert
HTTP 200 OK, 2 such POST requests observed in our test session. All 5 navigation targets (google.com, amazon.com, facebook.com, wikipedia.org, reddit.com) appeared in the payloads.
Headers
Content-Typeapplication/json
Cache-Controlno-cache
Body
{
  "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
    }
  ]
}
03EvidenceFIELD TABLE
What the extension sends for each page you visit:
FieldValueWhy it matters
Page you visited
https://en.wikipedia.org/wiki/Main_PageThe 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-0021deadbeefA UUID generated on first install and stored permanently. Links all your navigation records together across time.
Your country
GBRThree-letter country code obtained by querying your IP address at startup.
How you got there
linkWhether you clicked a link, typed the URL, or were redirected, reveals your browsing patterns.
Your operating system
WindowsOS name, sent with every batch.
Extension version
2.1.62Which version of Stands AdBlocker you have installed.
Timestamp
1745001234567When the page finished loading, in milliseconds.
04EvidenceCODE COMPARE
The code that does this

The navigation listener and payload assembly from the extension's source:

What it actually does
Tab navigation hook — runs on every tab URL change
// 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);
}
POST assembler — builds the JSON batch and sends it
// 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 })
  });
}
05EvidenceTHIRD PARTY LIST
Where your navigation data is sent:
  • 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.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-506
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You visit any website where the extension's proxy-apply.js script is loaded.

The extension did this

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.

02EvidenceCODE COMPARE
The code that does this

proxy-apply.js, Function.prototype.toString global patch:

What it actually does
Function.prototype.toString anti-detection patch — annotated
// 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;
03EvidenceCODE COMPARE
The code that does this

youtube/isolated.js, Map.prototype.has wrap and ytcfg user-agent injection (YouTube-only):

What it actually does
Map.prototype.has intercept on YouTube — annotated
// 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]);
  }
});
ytcfg user-agent modification — annotated
// 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]);
04EvidencePLAIN NOTE
Why global prototype modification is noteworthy

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.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
What actually happens
You did this

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 did this

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.

02EvidenceNETWORK CAPTURE
Captured request
POSThttps://prod.standsapp.org/api/v2/events
HTTP 200 OK, observed during dynamic analysis. Request body containing privateUserId and anonymousUserId was captured. (Body structure above is illustrative, reconstructed from source; field names and types confirmed by code.)
Headers
Content-Typeapplication/json
Body
{
  "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"
    }
  ]
}
03EvidenceFIELD TABLE
Fields included in each event POST to prod.standsapp.org:
FieldValueWhy it matters
Server-assigned user ID
b1c9f3a2-d45e-4812-a678-1234567890abA 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-0021deadbeefA 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.netChrome extension IDs of every other extension in your browser, included when the optional 'management' permission is granted.
Extension version
2.1.64Which version of Stands AdBlocker sent this log entry.
04EvidenceCODE COMPARE
The code that does this

ServerLogger.sendToServer, payload assembly and delivery:

What it actually does
Event upload — readable form showing identifier collection
// 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),
  });
}
URL-bearing event constructors
// 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 };
}
05EvidenceTHIRD PARTY LIST
Where event data is sent:
  • 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.

Updated 30 September 2026lgblnfidahcdcjddiepkckcfdhpknnjh