Is iGive Button safe?

Medium risk

iGive Button initializes PostHog analytics with autocapture on every page you visit, not just cashback merchant sites.

iGive Button's content script runs on every website and starts the PostHog analytics SDK before it checks whether the current page is a cashback merchant. PostHog's autocapture then records click and page-view events (including the page's URL and host) on all sites, tagged with either your logged-in member ID or a persistent installation identifier, and sends this data to us.posthog.com.

iGive.comv11.3.1Firefox Add-ons
53Risk
Who publishes it

iGive.com - no other listings under this identity, 7 shared hostnames

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
iGive.com

Shared hosts - 7 hostnames

Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.

igive.com
Also called by 2 other listings
images.igive.com
Also called by 2 other listings
ja.igive.com
Also called by 2 other listings, including iGive Button
sh.igive.com
Also called by 2 other listings, including iGive Button
support.igive.com
Also called by 2 other listings, including iGive Button
tm2.igive.com
Also called by 2 other listings, including iGive Button
backup-api.com
Also called by 4 other listings, including Citro

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

Publishers can request a review.

Findings

PostHog autocapture tracks every website you visit, not just merchant pages

Code analysis shows the content script, which runs on every page, starts PostHog autocapture before checking whether the page is an iGive merchant.

Clicks and page data from every site are sent to us.posthog.com.

Severity
High unwanted
Type
Unexpected
CWE
CWE-359
Source
Static analysis
What actually happens
You did this

You visit any website with the extension installed, whether or not it is one of iGive's cashback merchants.

The content script's manifest match pattern is *://*/*, so it loads on every page.

The extension did this

Before checking if the page belongs to a merchant, the extension starts PostHog with autocapture on and begins recording your clicks and the page's URL.

The merchant lookup that would gate the cashback UI runs only afterward, so tracking is not conditioned on it.

The code that does this

The content-script PostHog config is the only context without a session-recording guard

Readable version

Content-script entry point (init)

src/entries/contentScript/main.js
async function init() {    const app = f(ContentService);    const injections = f(InjectionControllerService);    await Promise.all([        injections.init(),        app.init()    ]);    await app.initPostHog();    await app.initSlider();}init();

getConfig() -- per-context PostHog settings

assets/posthog.service-D0TUZEMA.js
const context = getExtensionContext();switch (context) {  case ExtensionContext.ContentScript:    return {      ...shared,      capture_pageview: false,      autocapture: true,      persistence: "memory"    };  case ExtensionContext.ServiceWorker:    return {      ...shared,      persistence: "memory",      capture_pageview: false,      autocapture: false,      disable_session_recording: true,      disable_surveys: true    };  case ExtensionContext.Background:    return {      ...shared,      persistence: "localStorage",      capture_pageview: false,      autocapture: false,      disable_session_recording: true,      disable_surveys: true    };  case ExtensionContext.Popup:  case ExtensionContext.Options:  case ExtensionContext.Devtools:    return {      ...shared,      capture_pageview: true,      autocapture: true,      persistence: "localStorage",      disable_session_recording: true    };  default:    return {      ...shared,      capture_pageview: false,      autocapture: false,      persistence: "memory"    };}
Fields attached to every autocapture event sent from the content script
  • Your tracking ID
    8841207

    A persistent ID that ties every page you visit back to the same identity. It becomes your logged-in iGive member ID once you sign in.

  • Page you're on
    https://en.wikipedia.org/wiki/Cashback

    The full URL of the page, including path and query string, for every site you visit while the extension is active.

  • Site domain
    en.wikipedia.org

    The hostname of the page, sent alongside the full URL.

  • What you clicked
    $autocapture on button#search-submit

    PostHog's autocapture records clicks, form submits, and other page interactions on any page, not just merchant sites.

Where this data goes
    • us.posthog.com

    PostHog Inc's US-region analytics ingestion endpoint. Receives the autocapture events and tracking ID from every page load, not only merchant/checkout pages.

Reproduce it yourself

Downloads a published extension build and checks whether its PostHog getConfig() still gives the content-script context autocapture:true with no disable_session_recording flag, unlike the service-worker context.

Requires
  • Node.js 18+
  • unzip
verify-posthog-scope.js · js
#!/usr/bin/env node// verify-posthog-scope.js//// Downloads a published .xpi build of an extension and checks whether its// PostHog getConfig() switch gives the content-script context// autocapture:true with no disable_session_recording flag, while the// service-worker/background context sets that flag. Confirms the code-path// asymmetry described in this finding is present in the build you point it at.//// Usage: node verify-posthog-scope.js <xpi-download-url>const fs = require('fs');const path = require('path');const os = require('os');const https = require('https');const { execSync } = require('child_process');function download(url, dest) {  return new Promise((resolve, reject) => {    const file = fs.createWriteStream(dest);    https.get(url, (res) => {      if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {        file.close();        download(res.headers.location, dest).then(resolve, reject);        return;      }      res.pipe(file);      file.on('finish', () => file.close(resolve));    }).on('error', reject);  });}async function main() {  const url = process.argv[2];  if (!url) {    console.error('Usage: node verify-posthog-scope.js <xpi-download-url>');    process.exit(1);  }  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'xpi-check-'));  const xpiPath = path.join(tmp, 'extension.xpi');  console.log(`Downloading ${url} ...`);  await download(url, xpiPath);  console.log('Unpacking...');  const outDir = path.join(tmp, 'unpacked');  fs.mkdirSync(outDir);  execSync(`unzip -oq "${xpiPath}" -d "${outDir}"`);  const manifestPath = path.join(outDir, 'manifest.json');  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));  const csEntry = manifest.content_scripts && manifest.content_scripts[0];  console.log('content_scripts[0].matches:', csEntry && csEntry.matches);  let posthogFile = '';  try {    posthogFile = execSync(      `grep -rl "getExtensionContext" "${outDir}" || true`    ).toString().trim().split('\n')[0];  } catch (e) {    posthogFile = '';  }  if (!posthogFile) {    console.log('Could not locate a PostHog config bundle in this build; the code may have moved or the SDK may have been replaced.');    return;  }  const src = fs.readFileSync(posthogFile, 'utf8');  const switchIdx = src.indexOf('switch (context)') !== -1    ? src.indexOf('switch (context)')    : src.indexOf('switch(context)');  if (switchIdx === -1) {    console.log('Could not locate the getConfig() context switch in this build.');    return;  }  const snippet = src.slice(switchIdx, switchIdx + 2000);  const csIdx = snippet.indexOf('ExtensionContext.ContentScript');  const swIdx = snippet.indexOf('ExtensionContext.ServiceWorker');  const bgIdx = snippet.indexOf('ExtensionContext.Background');  if (csIdx === -1 || swIdx === -1) {    console.log('Could not locate ContentScript/ServiceWorker branches in this build.');    return;  }  const contentScriptBlock = snippet.slice(csIdx, swIdx);  const serviceWorkerBlock = snippet.slice(swIdx, bgIdx !== -1 ? bgIdx : swIdx + 400);  const csAutocapture = /autocapture:\s*true/.test(contentScriptBlock);  const csSessionGuard = /disable_session_recording/.test(contentScriptBlock);  const swSessionGuard = /disable_session_recording/.test(serviceWorkerBlock);  console.log('\n--- Result ---');  console.log('ContentScript branch sets autocapture:true          ->', csAutocapture);  console.log('ContentScript branch sets disable_session_recording ->', csSessionGuard);  console.log('ServiceWorker branch sets disable_session_recording ->', swSessionGuard);  if (csAutocapture && !csSessionGuard && swSessionGuard) {    console.log('\nCONFIRMED: this build ships the same code-path asymmetry described in the finding.');  } else {    console.log('\nThe pattern was not found as described; the code may have changed in this build.');  }}main().catch((err) => {  console.error(err);  process.exit(1);});
How to run it
  1. 1Get an .xpi download URL for the build to check.
  2. 2Run `node verify-posthog-scope.js <url>`.
  3. 3CONFIRMED means content-script still lacks the session-recording guard the service worker sets.
Observation

Static analysis finding. This behaviour was identified by reading the shipped extension code and has not yet been reproduced in a live run. The trigger conditions and the exact data sent are read from the code, not from an observed capture.

What it can do

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

    *://*/*

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

    tabs

  • Watch every request your browser makes

    webRequest

  • See every page you navigate to, as you navigate to it

    webNavigation

  • Store data in your browser

    storage

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

    cookies

  • Schedule its own background tasks

    alarms

Where it sends data

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

  • us.posthog.com

    iGive Button sends data to us.posthog.com. 5 other extensions we have analyzed send data here.

Updated 22 October 2026amo-653006