Is StayFocusd – Website Blocker & Focus Timer & Shorts Blocker safe?
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.…
Who publishes itSensor Tower - 5 other listings from the same operator, 3 of them carrying a finding
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.
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.
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
You browse the web with StayFocusd installed and accept its terms of service.
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.
200 OK (14 such requests observed across 2 dynamic analysis runs after ToS acceptance)
- 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
<binary protobuf payload, ~28-33 bytes — not human-readable>- Your install IDvw7asxlnvaqsj
A persistent identifier unique to your extension install. Ties every session upload to you across browser restarts.
- Session duration847 seconds
How long your browsing session lasted.
- Session timestamp1744640000 (Unix epoch)
When the browsing session started and ended.
- Browser and OSChrome / 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 ID33
Hardcoded identifier (33) marking these uploads as belonging to the SensorTower/StayFree panel research programme.
The endpoint constants and Ko() ad-finder setup from background.js:
// 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 SDK upload payload assembly (from ad-finder.js):
// 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 };}Intercepts StayFocusd's Ajax0001/IPD upload in Chrome DevTools and decodes the LZ-String compressed payload so you can read what was sent.
- Node.js 16+
- npm install lz-string
// 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);}- 1Capture a POST to api-pm.stayfreeapps.com/Ajax0001/IPD in Chrome DevTools.
- 2Copy the request body: right-click → Copy → Copy as base
- 36
- 4
- 5Run: node decode-ipd-payload.js <paste-base64-here>
- 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
You visit any web page while StayFocusd is installed.
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.
- Website you visitedamazon.com/dp/B0CX4KMQN3
The exact hostname and path of every page you open.
- Time spent on that page142
How many seconds you spent on the page before navigating away.
- Where you came fromgoogle.com/search?q=wireless+headphones
The page you visited immediately before, builds a chain of your browsing activity.
- UTM tracking tagsutm_source=newsletter, utm_campaign=spring_sale
Marketing campaign tags from the URL, e.g. which ad or newsletter sent you to the page.
- Ad networkgoogle
Which advertising network was associated with the page visit, if any.
- Your install IDvw7asxlnvaqsj
A persistent identifier unique to your extension install. Ties every record to you across sessions.
- Browser and OSChrome / Windows
Which browser and operating system you use.
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 upload configuration from background.js:
// 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 page views upload function from the SensorTower SDK chunk (_virtual_wxt-plugins-DMpaGf42.js):
// 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);- 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
You start Google sign-in from the StayFocusd options page.
The flow is tied to the extension's email-cleaner feature.
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.
- Gmail mailbox permissionhttps://www.googleapis.com/auth/gmail.readonly
Allows the sign-in grant to read Gmail messages for the signed-in Google account.
- Google OAuth client863918784766-pafcg7sntsktkeen0m3ujcjkrqp9oh2c.apps.googleusercontent.com
Identifies the Google application that receives the sign-in grant.
- Token exchange requestprovider=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 tokensaccessToken and refreshToken from the token response
Can authorize Gmail API access for the signed-in account until revoked or expired.
- Iframe token messagemethod=saveGoogleTokens; origin=https://app.stayfresh.email
Sends the saved Google tokens from the extension page into the email-cleaner iframe.
OAuth scope, token exchange, storage, and iframe forwarding
Readable token exchange and local storage
chunks/create-base-app-BB3ZXQy4.jsasync 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.jsasync 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.jsfunction 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)}- 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