Is StayFocusd – Website Blocker & Focus Timer & Shorts Blocker safe?

High risk

StayFocusd is high risk. StayFocusd uploads session data to api-pm.stayfreeapps.com/Ajax0001/IPD after you browse. Confirmed: 14 POSTs in two runs. Each ~28-33 byte payload carries install ID, session duration, device type, timezone. Run by SensorTower/StayFree.…

ST Pulsev4.6.14Chrome Web Store
79Risk
Who publishes it

Sensor Tower - 5 other listings from the same operator, 3 of them carrying a finding

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

Publisher
ST Pulse
Declared legal entity
Sensor Tower
Registered address
275 Battery St #800, San Francisco, CA 94111-3364, US
Registered contact
Sensor Tower, Inc.

Same store account

1 other listing published from this account, 200k+ users between them. 1 of them carries a finding.

Same operator - 4 listings

Published under a different store account, but sharing the registered address, contact or declared legal entity this one gave the store.

Shared hosts - 4 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.

app.stayfresh.email
Also called by 3 other listings, including StayFree - Web Analytics & Time Tracker
stayfreeapps.com
Also called by 3 other listings, including StayFree - Web Analytics & Time Tracker
purchases.stayfreeapps.com
Also called by 4 other listings, including StayFree - Web Analytics & Time Tracker
sensortower.com
Also called by 5 other listings, including Luna Adblock for Youtube & Websites

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

Publishers can request a review.

Findings

Session Data Uploaded to SensorTower Panel via Ajax0001/IPD

StayFocusd uploads session data to api-pm.stayfreeapps.com/Ajax0001/IPD after you browse.

Confirmed: 14 POSTs in two runs.

Each ~28-33 byte payload carries install ID, session duration, device type, timezone.

Run by SensorTower/StayFree.

Severity
High unwanted
Type
Unexpected
CWE
CWE-359
Source
Dynamic sandbox
What actually happens
You did this

You browse the web with StayFocusd installed and accept its terms of service.

The extension did this

StayFocusd automatically sends your session data to a SensorTower analytics server, 14 POST requests were captured in a single analysis session.

Each request uses the opaque Ajax0001/IPD endpoint with a binary (likely protobuf) body that cannot be read in plain text.

Captured request
POSThttps://api-pm.stayfreeapps.com/Ajax0001/IPD

200 OK (14 such requests observed across 2 dynamic analysis runs after ToS acceptance)

Headers
User-Agent
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Content-Type
application/octet-stream
Body
<binary protobuf payload, ~28-33 bytes — not human-readable>
Data known to be included in session uploads (from source code and SDK internals)
  • Your install ID
    vw7asxlnvaqsj

    A persistent identifier unique to your extension install. Ties every session upload to you across browser restarts.

  • Session duration
    847 seconds

    How long your browsing session lasted.

  • Session timestamp
    1744640000 (Unix epoch)

    When the browsing session started and ended.

  • Browser and OS
    Chrome / Linux

    Which browser (Chrome) and operating system you are using.

  • Birth year (if provided)
    1992

    If you entered your birth year in StayFocusd settings, it is included in every upload.

  • Panel partner ID
    33

    Hardcoded identifier (33) marking these uploads as belonging to the SensorTower/StayFree panel research programme.

The code that does this

The endpoint constants and Ko() ad-finder setup from background.js:

Readable version
// Hardcoded SensorTower panel endpointsconst SESSIONS_UPLOAD_URL = "https://api-pm.stayfreeapps.com/Ajax0001/IPD";const REMOTE_CONFIG_URL   = "https://api-pm.stayfreeapps.com/Ajax0001/Config";const PANEL_PARTNER_ID    = 33; // SensorTower/StayFree panel IDfunction setupAdFinderSDK() {  const settings = getUserSettings();  const { processAdFinderMessage } = createAdFinder({    async isEnabled() { return await settings.canUploadData(); },    getInstallId: () => getInstallId(),    extensionVersion: "4.5.2",    extensionId: "laankejkbhbdhmipfmgcngdelahlfoji",    crawlUploadUrl: SESSIONS_UPLOAD_URL,    // Ajax0001/IPD    pmExternalConfigUrl: REMOTE_CONFIG_URL, // Ajax0001/Config    panelPartnerId: PANEL_PARTNER_ID,    logger: logger  });  // Forward ad-finder messages from content scripts  onMessage("@sensortower/ad-finder#MESSAGE", ({ data }) => processAdFinderMessage(data));}
The code that does this

The SDK upload payload assembly (from ad-finder.js):

Readable version
// Injects panel metadata into every upload before compressionprepareData(rawPayload) {  return {    ...rawPayload,    PartnerVersion:   chrome.runtime.getManifest().version,  // "4.5.2"    ExtensionId:      chrome.runtime.id,    ExtensionVersion: this.config.extensionVersion,    UserAgent:        navigator.userAgent,    BrowserLanguage:  navigator.language,    BlankZys:         null,    PanelPartnerId:   this.config.panelPartnerId  // 33  };}
Reproduce it yourself

Intercepts StayFocusd's Ajax0001/IPD upload in Chrome DevTools and decodes the LZ-String compressed payload so you can read what was sent.

Requires
  • Node.js 16+
  • npm install lz-string
decode-ipd-payload.js · js
// decode-ipd-payload.js// Decodes a captured Ajax0001/IPD upload from StayFocusd.// The payload is LZ-String compressed JSON posted as application/octet-stream.//// Install: npm i lz-string// Usage:   node decode-ipd-payload.js <base64-of-body>const LZString = require('lz-string');// Paste your captured binary body as base64 here, or pass as argvconst base64Body = process.argv[2] || '<paste-base64-body-here>';try {  // Convert base64 to Uint8Array  const buf = Buffer.from(base64Body, 'base64');  const uint8 = new Uint8Array(buf);  // Decompress with LZ-String (decompressFromUint8Array)  const json = LZString.decompressFromUint8Array(uint8);  if (!json) {    console.error('Decompression failed — ensure you are using the correct LZ-String variant');    process.exit(1);  }  const obj = JSON.parse(json);  console.log('Decoded payload:');  console.log(JSON.stringify(obj, null, 2));  // Highlight key fields  console.log('\n--- Key identifiers ---');  if (obj.PanelPartnerId) console.log('PanelPartnerId:', obj.PanelPartnerId);  if (obj.ExtensionId)    console.log('ExtensionId:', obj.ExtensionId);  if (obj.UserAgent)      console.log('UserAgent:', obj.UserAgent);} catch (err) {  console.error('Error decoding payload:', err.message);  process.exit(1);}
How to run it
  1. 1Capture a POST to api-pm.stayfreeapps.com/Ajax0001/IPD in Chrome DevTools.
  2. 2Copy the request body: right-click → Copy → Copy as base
  3. 36
  4. 4
  5. 5Run: node decode-ipd-payload.js <paste-base64-here>
Where session data is sent
    • api-pm.stayfreeapps.com

    SensorTower/StayFree panel analytics backend. Ajax0001/IPD receives compressed session and ad-crawl data; stayfreeapps.com is run by SensorTower, an app market intelligence firm.

Browsing History Uploaded Every 5 Minutes via SensorTower SDK

StayFocusd bundles a SensorTower/StayFree SDK that records every site you visit, hostname, path, time on page, referrer, and UTM tags, without notifying you.

Data batches and uploads every 5 minutes.

Two 7-minute tests didn't see it fire.

Severity
High unwanted
Type
Unexpected
CWE
CWE-359
Source
Dynamic sandbox
What actually happens
You did this

You visit any web page while StayFocusd is installed.

The extension did this

StayFocusd records the hostname, path, how long you stayed, and where you came from, then uploads a batch to a SensorTower analytics server every five minutes.

Data collection happens on every page, not just sites you have blocked or tracked in StayFocusd.

What the extension collects per page visit
  • Website you visited
    amazon.com/dp/B0CX4KMQN3

    The exact hostname and path of every page you open.

  • Time spent on that page
    142

    How many seconds you spent on the page before navigating away.

  • Where you came from
    google.com/search?q=wireless+headphones

    The page you visited immediately before, builds a chain of your browsing activity.

  • UTM tracking tags
    utm_source=newsletter, utm_campaign=spring_sale

    Marketing campaign tags from the URL, e.g. which ad or newsletter sent you to the page.

  • Ad network
    google

    Which advertising network was associated with the page visit, if any.

  • Your install ID
    vw7asxlnvaqsj

    A persistent identifier unique to your extension install. Ties every record to you across sessions.

  • Browser and OS
    Chrome / Windows

    Which browser and operating system you use.

When this fires

Every 5 minutes

Page view data is stored locally in the browser's IndexedDB and flushed to the server every 5 minutes. During analysis, the upload fired on a 2-second interval when Chrome was running in headless mode, suggesting the interval is shortened for automated/testing environments.

The code that does this

The upload configuration from background.js:

Readable version
// Upload interval: 5 minutes (or 2 seconds in headless Chrome)const UPLOAD_INTERVAL_MS = 5 * 60 * 1000;function setupUsageUploader() {  const db = getDatabase();  const settings = getUserSettings();  const isHeadless = navigator.userAgent.toLowerCase().includes('headlesschrome');  const { sessionMonitor, usageUploader } = createUploader({    db,    uploads: {      // Headless detection: use 2s interval in automated environments      uploadIntervalInMs: isHeadless ? 2000 : UPLOAD_INTERVAL_MS,      getInstallId: getInstallId,      api: getPanelApiClient(),   // points to api-pm.stayfreeapps.com      getBirthYear: getBirthYear, // optional, from user settings      enabled: () => settings.canUploadData(),      getAppId: () => 'laankejkbhbdhmipfmgcngdelahlfoji',      pageViews: {        getUploadIgnoreList: () => getRemoteConfig().pageViewIgnoreList      }    },    getAdNetworks: () => getRemoteConfig().adNetworks ?? []  });  return { sessionMonitor, usageUploader };}
The code that does this

The page views upload function from the SensorTower SDK chunk (_virtual_wxt-plugins-DMpaGf42.js):

Readable version
// Builds and sends the page-views payloadconst parsedUA = parseUserAgent(navigator.userAgent);const payload = {  app_id:                  params.appId,          // "laankejkbhbdhmipfmgcngdelahlfoji"  install_id:              params.installId,       // persistent per-install UUID  time_zone:               getTimezone(),          // e.g. "GMT+01:00"  device_name:             parsedUA.browser.name,  // "Chrome"  device_type:             parsedUA.os.name,       // "Windows"  birth_year:              params.birthYear,       // optional, from user  websites:                params.websites,        // per-hostname/path duration records  diff_private_websites:   params.diffPrivateWebsites};await apiClient.post('/v1/page_views/upload', payload);
Where your browsing history ends up
    • api-pm.stayfreeapps.com

    SensorTower/StayFree panel analytics backend. Receives full browsing duration records. stayfreeapps.com is operated by SensorTower, the app analytics firm that acquired StayFree.

    • stayfocusd.st-panel-api.com

    Alternate domain observed during dynamic analysis for the same StayFree panel API. Confirmed to receive query_params/upload traffic in practice.

Gmail OAuth Tokens Forwarded to StayFresh Iframe

StayFocusd's sign-in requests read-only Gmail access, exchanges the code with purchases.stayfreeapps.com, stores tokens, forwards both to an app.stayfresh.email iframe on the cleaner UI.

DA saw the OAuth popup; Google blocked on mismatch.

Severity
High unwanted
Type
Unexpected
CWE
CWE-522
Source
Dynamic sandbox
What actually happens
You did this

You start Google sign-in from the StayFocusd options page.

The flow is tied to the extension's email-cleaner feature.

The extension did this

The extension asks for read-only Gmail access, stores the returned tokens, and forwards them to a StayFresh iframe.

The forwarding happens when the email-cleaner iframe finishes loading.

OAuth and token fields handled by the extension
  • Gmail mailbox permission
    https://www.googleapis.com/auth/gmail.readonly

    Allows the sign-in grant to read Gmail messages for the signed-in Google account.

  • Google OAuth client
    863918784766-pafcg7sntsktkeen0m3ujcjkrqp9oh2c.apps.googleusercontent.com

    Identifies the Google application that receives the sign-in grant.

  • Token exchange request
    provider=google; app=stayfocusd; platform=web-extension; redirectUri=https://laankejkbhbdhmipfmgcngdelahlfoji.chromiumapp.org/

    Sends the returned Google authorization code to the StayFocusd purchase API for token exchange.

  • Returned Google tokens
    accessToken and refreshToken from the token response

    Can authorize Gmail API access for the signed-in account until revoked or expired.

  • Iframe token message
    method=saveGoogleTokens; origin=https://app.stayfresh.email

    Sends the saved Google tokens from the extension page into the email-cleaner iframe.

Captured request
POSThttps://purchases.stayfreeapps.com/api/v2/auth/token
The code that does this

OAuth scope, token exchange, storage, and iframe forwarding

Readable version

Readable token exchange and local storage

chunks/create-base-app-BB3ZXQy4.js
async function v() {  a.value = true;  try {    const {      code: u,      redirectUri: m    } = await t.performLogin(), w = await t.getInstallId(), _ = await t.getBirthYear(), {      data: R,      error: I    } = await e.POST("/api/v2/auth/token", {      body: {        code: u,        provider: "google",        app: "stayfocusd",        platform: r,        redirectUri: m,        installId: w,        optIn: {          birthYear: _        }      }    });    if (I) throw new Error(I.message ?? "Token exchange failed");    o.value = R;    const {      data: x,      error: Q    } = await e.GET("/api/v2/auth/session");    if (Q) throw new Error("Failed to fetch session info");    s.value = x, await n.setSession(x), await n.setCookie(R), b()  } finally {    a.value = false  }}const pr = "863918784766-pafcg7sntsktkeen0m3ujcjkrqp9oh2c.apps.googleusercontent.com",  gr = ["https://www.googleapis.com/auth/gmail.readonly"],  mr = "https://purchases.stayfreeapps.com",  Fr = "https://app.stayfresh.email";

Readable Google OAuth launch

chunks/create-base-app-BB3ZXQy4.js
async performLogin() {  const e = Me.identity.getRedirectURL(),    n = new URL("https://accounts.google.com/o/oauth2/v2/auth");  n.searchParams.set("client_id", pr), n.searchParams.set("redirect_uri", e), n.searchParams.set("response_type", "code"), n.searchParams.set("scope", gr.join(" ")), n.searchParams.set("access_type", "offline"), n.searchParams.set("prompt", "consent");  const r = await Me.identity.launchWebAuthFlow({    url: n.toString(),    interactive: true  });  if (!r) throw new Error("No response from auth flow");  const s = new URL(r).searchParams.get("code");  if (!s) throw new Error("No authorization code in response");  return {    code: s,    redirectUri: e  }}

Readable iframe token message

chunks/email-cleaner-D7ynmxUX.js
function d() {  setTimeout(() => {    x(), r.value && g({      method: "saveGoogleTokens",      args: {        accessToken: r.value?.accessToken,        refreshToken: r.value?.refreshToken      }    }), n.value = false  }, 2 * B.Second)}function x() {  g({    method: "setLocale",    args: {      languageCode: f.value    }  })}function g(t) {  const a = JSON.stringify(t);  s.value?.contentWindow?.postMessage(a, l)}
External services receiving the OAuth flow or token message
    • accounts.google.com

    Google OAuth authorization page opened by the extension.

    • purchases.stayfreeapps.com

    Receives the authorization code and returns the token response for StayFocusd.

    • app.stayfresh.email

    Iframe origin that receives the saved Google access and refresh tokens.

+3 more findings not shown

Our write-ups

Updated 30 September 2026laankejkbhbdhmipfmgcngdelahlfoji