Is StayFree - Website Blocker, Web Usage Stats, Shorts Blocker safe?

High risk

StayFree is high risk. Two GET requests to api.stayfreeapps.com fire within one second of load, before interaction. They set which AI chats are monitored, ad-detection patterns, tracking exclusions and query-param collection, so behavior changes without update.…

ST Pulsev2.9.6Chrome Web Store
75Risk

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

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-829
SourceAI SANDBOX

Remote Config Fetched at Startup Without User Action

Two GET requests to api.stayfreeapps.com fire within one second of load, before interaction.

They set which AI chats are monitored, ad-detection patterns, tracking exclusions and query-param collection, so behavior changes without update.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You install StayFree and Chrome starts the extension.

The extension did this

The extension immediately fetches its configuration from api.stayfreeapps.com before you interact with it.

Two GET requests fire within one second of startup. The responses control which AI services StayFree monitors, which ad patterns it detects, which sites are excluded from tracking, and whether query string collection is active.

02EvidenceNETWORK CAPTURE
Captured request
GEThttps://api.stayfreeapps.com/v1/remote_config/stayfree-chrome
JSON object containing genAiParsers (DOM selectors per AI chat service), chatbotSelectors, adNetworks (regex patterns), pageViewIgnoreList (sites excluded from tracking), uploadWebUsage flag, sampleIntegerPercent (upload sampling rate). Observed in both independent DA runs, firing within 1 second of extension load.
03EvidenceFIELD TABLE
What the remote config controls:
FieldValueWhy it matters
AI chat monitoring selectors
{ "chatgpt": { "userMessageSelector": "div.text-message--user" } }DOM selectors for reading your messages from AI chats (ChatGPT, Claude, Gemini, Gorgias, others). The server can add new services anytime.
Ad detection patterns
{ "name": "DoubleClick", "regexes": ["doubleclick\\.net"] }Regex patterns used to identify ads on pages you visit. These determine which ad networks trigger the extension's ad-count features.
Site exclusion list
[ "mail.google.com/**", "webmd.com/**", "mayoclinic.com/**" ]Domains excluded from usage tracking. The server controls this list; sites left off it have their query parameters collected.
Upload enabled flag
trueWhether query string collection is active. The server can turn this on or off without an extension update.
Upload sampling rate
100An integer percentage controlling what fraction of captured data is sent. The server controls this value.
04EvidenceCODE COMPARE
The code that does this

The remote config fetch logic from the extension's source:

What it actually does
Shared-web-config fetch + persistence
// Constant for the shared remote config endpoint
const SHARED_CONFIG_URL = 'https://api.stayfreeapps.com/v1/remote_config/shared-web-config';

// Sets up a config fetcher that:
//   1. GETs both SHARED_CONFIG_URL and the stayfree-chrome URL on startup
//   2. Persists the last-fetched time and response to chrome.storage
//   3. Re-fetches on @alarm/update-remote-config alarm
function createRemoteConfigClient({ storage, urls, defaultConfig }) {
  const fetcher = createConfigFetcher({
    urls: [SHARED_CONFIG_URL, ...urls],
    defaultConfig,
    persistConfig: (cfg) => storage.setItem('remote-config', cfg),
    persistLastFetchedTime: (t) => storage.setItem('remote-config-last-fetched-at', t),
    restoreConfig: async () => (await storage.getItem('remote-config')) ?? undefined,
    restoreLastFetchedTime: async () => (await storage.getItem('remote-config-last-fetched-at')) ?? undefined,
  });
  return {
    fetchLatest: fetcher.fetchLatest,
    addChangeListener: fetcher.addChangeListener,
    getLastFetchedTime: () => fetcher.lastFetchedTime,
    getValue: () => fetcher.value,
  };
}
StayFree-chrome config URL (controls AI parsers, ad networks)
// Second remote config endpoint — controls genAiParsers, chatbotSelectors,
// adNetworks, pageViewIgnoreList, uploadWebUsage, sampleIntegerPercent.
const STAYFREE_CHROME_CONFIG_URL = 'https://api.stayfreeapps.com/v1/remote_config/stayfree-chrome';
05EvidenceTHIRD PARTY LIST
Where the config request goes:
  • api.stayfreeapps.com

    StayFree's own API server. Receives no user data in this request, but controls the extension's data-collection behavior through its response.

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

URL Query Strings from All Navigations Uploaded to StayFree Servers

A canary term on amazon.com/google.com appeared 5 min later in a POST to api.stayfreeapps.com/v1/query_params/upload, with params from other visited sites.

Uploads recur every 5 min.

Redaction misses search terms and session IDs.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You search on Google, Amazon, or any other site, or visit any URL that contains a query string.

This includes search terms, filter parameters, OAuth state tokens, and session identifiers in the URL.

The extension did this

StayFree records the query parameters and, every five minutes, uploads the batch to api.stayfreeapps.com.

Our dynamic analysis captured a canary search term (a planted marker value) in the upload body, confirming the collection is live.

02EvidenceNETWORK CAPTURE
Captured request
POSThttps://api.stayfreeapps.com/v1/query_params/upload
HTTP 200. Body confirmed to contain a planted marker value during dynamic analysis. Body size 3017 bytes.
Body
{
  "app_id": "elfaihghhjjoknimpccccmkioofjjfkf",
  "install_id": "ujyruflyoc3i0",
  "time_zone": "America/New_York",
  "country_code": "US",
  "websites": {
    "amazon.com": {
      "/s": {
        "query_params": [
          {
            "name": "k",
            "value": "BIRD_12345_canary",
            "timestamp": 1744640200
          },
          {
            "name": "k",
            "value": "wireless headphones",
            "timestamp": 1744639850
          }
        ]
      }
    },
    "google.com": {
      "/search": {
        "query_params": [
          {
            "name": "q",
            "value": "BIRD_12345_canary",
            "timestamp": 1744640210
          },
          {
            "name": "q",
            "value": "best noise cancelling headphones 2026",
            "timestamp": 1744639900
          }
        ]
      }
    },
    "reddit.com": {
      "/search": {
        "query_params": [
          {
            "name": "q",
            "value": "headphones",
            "timestamp": 1744639950
          }
        ]
      }
    },
    "youtube.com": {
      "/results": {
        "query_params": [
          {
            "name": "search_query",
            "value": "headphone review",
            "timestamp": 1744640050
          }
        ]
      }
    }
  }
}
03EvidenceFIELD TABLE
What StayFree sends in each upload:
FieldValueWhy it matters
Your install ID
ujyruflyoc3i0A persistent identifier tied to your copy of StayFree. Every upload uses the same ID, linking all your search history over time.
Search query
best noise cancelling headphones 2026The exact text you searched for on each site, what you looked up on Google, Amazon, YouTube, Reddit, and any other site with a query string.
Site and path
google.com/searchThe hostname and path where the query was made, so the server knows which search engine or site each query came from.
Timestamp of each query
1744640210When you made each search, recorded in seconds since the Unix epoch.
App ID
elfaihghhjjoknimpccccmkioofjjfkfThe extension's ID, identifying which client is sending the data.
Country code and time zone
US / America/New_YorkYour country and local time zone, sent with every upload.
04EvidenceCODE COMPARE
The code that does this

The query parameter collector and upload function from the extension's source:

What it actually does
webNavigation.onCompleted listener — collects and batches query params
// Registers on every navigation completion.
// Skips: disabled by remote config, URLs with no query string, domains in pageViewIgnoreList.
// Applies partial PII redaction (_C) before storing.
function setupQueryParamCollector({ api, appId, installId, enabled, urlIgnorelist, storageKey, uploadInterval, countryCode }) {
  // Schedule periodic batch upload every 5 minutes
  scheduleUploadAlarm(storageKey, uploadInterval, api.uploadQueryParams);

  chrome.webNavigation.onCompleted.addListener(async (navEvent) => {
    try {
      if (!await enabled()) return;          // gated by remote config uploadWebUsage flag

      const parsed = parseUrl(navEvent.url);
      if (!parsed) return;

      const { hostname, path } = parsed;
      const url = new URL(navEvent.url);
      const fullPath = `${hostname}${path}`;

      // Skip domains in pageViewIgnoreList (e.g. mail.google.com, webmd.com)
      if (urlIgnorelist.some(pattern => matchGlob(fullPath, pattern))) return;

      const timestamp = Math.round(Date.now() / 1000);
      const rawParams = Object.fromEntries(url.searchParams.entries());

      // Apply partial PII redaction — strips emails, phone numbers, SSNs,
      // person names, social media mentions, and URLs.
      // Short values like search terms or OAuth codes pass through.
      const queryParams = Object.entries(rawParams).map(([name, value]) => ({
        name,
        value: redactPii(value),
        timestamp,
      }));

      if (queryParams.length === 0) return;

      const batch = { appId, installId, countryCode, websites: { [hostname]: { [path]: { query_params: queryParams } } } };
      await appendToLocalStorage(storageKey, batch);
    } catch (err) {
      console.error('Error storing query params:', err);
    }
  });
}
Upload function — POSTs the batched data
// Called by the 5-minute alarm. Reads the batch from storage and POSTs it.
async function uploadQueryParams({ appId, installId, countryCode, websites }) {
  const body = {
    app_id: appId,
    install_id: installId,
    time_zone: getTimezone(),       // e.g. 'America/New_York'
    country_code: countryCode,
    websites: websites,             // { hostname: { path: { query_params: [...] } } }
  };
  await apiClient.post('/v1/query_params/upload', { method: 'POST', retry: 0, body });
}
05EvidenceSTORAGE DUMP
What's stored on your device

Before uploading, query parameters buffer here in local storage until the 5-minute alarm fires, then they're POSTed and the key cleared.

Locationchrome.storage.local key '@stayfree/query-parameters'
Contents (JSON)
{
  "appId": "elfaihghhjjoknimpccccmkioofjjfkf",
  "websites": {
    "amazon.com": {
      "/s": {
        "query_params": [
          {
            "name": "k",
            "value": "wireless headphones",
            "timestamp": 1744639850
          },
          {
            "name": "k",
            "value": "BIRD_12345_canary",
            "timestamp": 1744640200
          }
        ]
      }
    },
    "google.com": {
      "/search": {
        "query_params": [
          {
            "name": "q",
            "value": "best noise cancelling headphones 2026",
            "timestamp": 1744639900
          },
          {
            "name": "q",
            "value": "BIRD_12345_canary",
            "timestamp": 1744640210
          }
        ]
      }
    }
  },
  "installId": "ujyruflyoc3i0",
  "countryCode": "US"
}
06EvidenceTHIRD PARTY LIST
Where your search queries are sent:
  • api.stayfreeapps.com

    StayFree's own API server. Receives the batched query parameter upload containing search terms, filter values, and other URL query strings from every non-excluded site you visit.

07EvidenceARTIFACT
Check if you're affected

A shell script that uses mitmproxy to capture and display any POSTs to api.stayfreeapps.com/v1/query_params/upload while you browse. Run it alongside Chrome, navigate to a few sites with search queries, and wait up to 5 minutes to see your search terms appear in the captured request body.

Requiresmitmproxy (pip install mitmproxy)Chrome with mitmproxy CA certificate trustedStayFree extension installed and data collection consent accepted
stayfree-query-canary.sh · sh
#!/usr/bin/env bash
# stayfree-query-canary.sh
# Captures StayFree's query-param upload using mitmproxy.
# Run: bash stayfree-query-canary.sh
# Then open Chrome with the mitmproxy CA trusted and StayFree installed.
# Navigate to google.com (/search?q=YOUR_CANARY_TERM) and wait up to 5 minutes.

set -euo pipefail

CANARY="canary_$(date +%s)"
OUTFILE="/tmp/stayfree_capture_$$.json"

echo "[*] Canary term: $CANARY"
echo "[*] Search for '$CANARY' on google.com and/or amazon.com"
echo "[*] Waiting for POST to /v1/query_params/upload (up to 5 minutes)..."
echo

# Inline mitmproxy script to filter relevant requests
cat > /tmp/sf_addon_$$.py << 'PYEOF'
import json, sys
from mitmproxy import http

def response(flow: http.HTTPFlow):
    if (
        'api.stayfreeapps.com' in flow.request.host
        and '/v1/query_params/upload' in flow.request.path
        and flow.request.method == 'POST'
    ):
        try:
            body = json.loads(flow.request.get_text())
            print("\n[CAPTURED] POST /v1/query_params/upload")
            print(json.dumps(body, indent=2))
            sys.stdout.flush()
        except Exception as e:
            print(f"[CAPTURED] raw body: {flow.request.get_text()[:2000]}")
PYEOF

mitmdump -s /tmp/sf_addon_$$.py -p 8080 2>&1
How to run it
  1. 1
    pip install mitmproxy.
  2. 2
    Trust its CA in Chrome.
  3. 3
    Run: bash stayfree-query-canary.sh.
  4. 4
    Set Chrome proxy to localhost:
  5. 5
    8
  6. 6
    0
  7. 7
    8
  8. 8
  9. 9
    Search a unique term on google.com.
  10. 10
    Wait up to 5 min.
  11. 11
    Script prints the POST body when upload fires.
SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Ad-finder SDK uploads crawl data to SensorTower

The Pathmatics/SensorTower ad-finder contacts api-pm.stayfreeapps.com and posts encoded crawl payloads while active.

It injects into supported pages, forwards crawl messages to background, adds context and uploads the compressed payload.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You browse the web with the extension enabled.

The extension did this

The extension runs an ad-finder SDK that sends encoded ad crawl data to the SensorTower/Pathmatics endpoint.

02EvidenceFIELD TABLE
Fields added or forwarded before the ad crawl upload
FieldValueWhy it matters
Page URL context
https://www.youtube.com/watch?v=dQw4w9WgXcQShows the page where the ad-finder crawl was running.
Ad crawl payload
crawlId=1714040700000-abc123, zys=[ad creative details]Contains the ad-finder's crawl result for ad creatives or related page content.
Extension identity
elfaihghhjjoknimpccccmkioofjjfkf, version 2.9.2Identifies which browser extension and version produced the upload.
Browser context
Mozilla/5.0 Macintosh; en-USAdds browser fingerprinting context that can help distinguish devices and environments.
Panel partner ID
30Marks the upload as belonging to a specific measurement panel integration.
03EvidenceNETWORK CAPTURE
Captured request
GEThttps://api-pm.stayfreeapps.com/Ajax0001/Config?installId=ckiw1mbhsqi4d
The observed configuration response identified panelPartnerId=30 for the SensorTower/Pathmatics integration.
04EvidenceNETWORK CAPTURE
Captured request
POSThttps://api-pm.stayfreeapps.com/Ajax0001/IPD
Dynamic analysis observed repeated POST requests with binary encoded bodies between 23 and 56 bytes.
Headers
Content-Typeapplication/octet-stream
05EvidenceCODE COMPARE
The code that does this

Ad-finder crawl messages and compressed IPD upload

What it actually does
Readable content-script crawl forwardingdeobfuscated/content-scripts/ad-finder.js
setupMessageListeners() {
  if (Ca.storage == null) throw Error("The storage permission is required for pathmatics' ad-finder");
  this.messenger.onMessage(jF, async e => {
    const i = await dF(await this.sendMessageToBackground({
      type: "ads#FETCH_HTTP_RESPONSE",
      payload: e
    }));
    return {
      response: i,
      transferables: [i]
    }
  }), $g() && (this.messenger.onMessage(AF, async () => ({
    response: await this.config.getInstallId()
  })), this.messenger.onMessage(zF, async e => {
    const i = el(e);
    if (i == null) return;
    const a = i.crawlId;
    let n = i.zys ?? [];
    if (typeof n == "string" && (n = el(n)), n == null) return;
    const s = Date.now();
    for (const r of n) {
      const o = {
        id: `${a}-${r.hash}`,
        type: "pathmatics",
        timestamp: s,
        details: r
      };
      this.config.logger?.log("Ad detected", o);
      try {
        await this.config.onAdDetected?.(o)
      } catch (l) {
        this.config.logger?.error("onAdDetected failed", {
          err: l
        })
      }
    }
    await this.sendMessageToBackground({
      type: "ads#CRAWL",
      payload: i
    }).catch(this.config.logger?.error)
  }), this.messenger.onMessage(TF, async () => ({
    response: await _g(this.sendMessageToBackground)
  })))
}
Readable upload clientdeobfuscated/background.js
async upload(e) {
  if (!await this.config.isEnabled()) return;
  const t = this.prepareData(e);
  this.config.logger?.log("Uploading data:", t);
  const a = await this.fetch(this.config.crawlUploadUrl, {
    method: "POST",
    body: T2.compressToUint8Array(JSON.stringify(t)),
    headers: {
      "Content-Type": "application/octet-stream"
    }
  }).then(i => this.config.logger?.log({
    status: i.status
  })).catch(this.config.logger?.error);
  this.config.logger?.log("Response:", a)
}
prepareData(e) {
  return {
    ...e,
    PartnerVersion: Ke.runtime.getManifest().version,
    ExtensionId: Ke.runtime.id,
    ExtensionVersion: this.config.extensionVersion,
    UserAgent: navigator.userAgent,
    BrowserLanguage: navigator.language,
    BlankZys: null,
    PanelPartnerId: this.config.panelPartnerId
  }
}
Readable background message handlerdeobfuscated/background.js
function HR(e) {
  const t = SR(e),
    a = new xR(t);
  x2(n => {
    const r = s => {
      throw t.logger?.error("Error handling message from ad-finder: " + JSON.stringify(n), s), s
    };
    switch (n.type) {
      case "ads#CRAWL":
        return a.upload(n.payload).catch(r);
      case "ads#GET_EXTERNAL_CONFIGURATION":
        return Promise.resolve(e.getInstallId()).then(s => $R(`${e.pmExternalConfigUrl}?installId=${s}`)).catch(r);
      case "ads#FETCH_HTTP_RESPONSE":
        return fetch(n.payload).then(s => {
          const o = parseInt(s.headers.get("Content-Length") ?? "");
          return !isNaN(o) && o > 1 << 25 ? new ArrayBuffer(0) : s.arrayBuffer()
        }).then(s => s.byteLength > 1 << 25 ? D2(new ArrayBuffer(0)) : D2(s));
      default:
        return Ju
    }
  }, t.logger)
}
06EvidenceTHIRD PARTY LIST
Pathmatics/SensorTower ad measurement host
  • api-pm.stayfreeapps.com

    Receives /Ajax0001/IPD encoded ad crawl uploads and serves /Ajax0001/Config panel configuration.

Our write-ups

Updated 10 September 2026elfaihghhjjoknimpccccmkioofjjfkf