Is Web Guardian: Phishing & Malware Protection safe?
Web Guardian tracks every site you visit and uploads session, page-view, and on-page text data to its own servers by default.
A background service worker records how long you spend on each site and each page you visit (with referrer and ad/UTM parameters), and a content script scans the visible text of every page against a remote brand-name list; both are uploaded to webguardian.st-panel-api.com, gated by a data-collection toggle that is turned on by default in the welcome screen. On Walmart, Target, and Instacart search pages, a bundled ad-finder SDK also scrapes sponsored product listings and the user's search term and uploads them to the same backend every few seconds. A separate 'Sign in with Google' flow, unrelated to phishing/malware protection, requests read access to the user's Gmail and ties the resulting session to purchases.stayfreeapps.com.
Strong link to a flagged operator: 2 of the 6 other listings from the same operator are rated high or critical for malicious behavior. Flagged: StayFocusd (high), Luna Adblock (high).
Who publishes itSensorTower Inc. - 6 other listings from the same operator, 4 of them carrying a finding
SensorTower Inc. - 6 other listings from the same operator, 4 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
2 other listings published from this account, 100k+ 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 - 5 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
Web Guardian logs every site you visit, with collection on by default
The extension logs a session and page view, with referrer and UTM tags, for every site you visit, then uploads batches every 5 minutes to a Sensor Tower endpoint tied to your install ID.
The onboarding checkbox for this starts checked.
What actually happens
You browse to any website with the extension installed and leave the onboarding data-sharing checkbox as it was.
That checkbox starts checked, so no action is required to leave it enabled.
The background service worker logs a session and page-view record for that site and later uploads it to a Sensor Tower analytics endpoint tied to your install ID.
The upload also carries the page's referrer and any UTM campaign parameters.
Fields included in each upload
- Install ID8f2e1a90-6c4d-4b3f-9a21-77e0c4d8b615
A persistent identifier that ties every site you visit, across every session, back to the same you.
- Website visitedwww.chase.com
The hostname of each site you browse to, logged for every page you view.
- Session duration182 seconds, 2026-09-18T14:02:11Z
How long you stayed on that site, with a timestamp.
- Referrer and campaign tagsutm_source=newsletter&utm_campaign=fall_sale
Where you came from and any UTM marketing tags on the link you followed.
- Birth year1991
An optional age indicator you can give during onboarding.
The code that does this
The upload function and the checkbox that gates it
uploadSessions, annotated
background.js// uploadSessions -- background.js, function bound as api.uploadSessions.// Called every ~5 minutes by the upload scheduler (uploadIntervalInMs: 3e5// == 300000ms, background.js:19063), batching whatever sessions/page views// queued locally since the last run.async function uploadSessions(params) { if (!params.appId.trim()) throw new Error("Missing required parameter 'appId'"); if (!params.installId.trim()) throw new Error("Missing required parameter 'installId'"); const ua = parseUserAgent(navigator?.userAgent); await panelApiClient.request(`/v1/web/upload`, { method: "POST", retry: 0, body: { app_id: params.appId, install_id: params.installId, // persistent per-install identifier time_zone: getTimeZone(), device_name: normalizeBrowserName(ua.browser?.name), device_type: ua.os.name, birth_year: params.birthYear, // optional, collected during onboarding websites: params.websites, // { hostname: { sessions: [{duration,timestamp}] } } diff_private_websites: params.diffPrivateWebsites, }, });}// baseURL for this client is set to https://webguardian.st-panel-api.com// (background.js:19023). A sibling function uploadPageViews posts the same// shape to /v1/page_views/upload with referrer/UTM fields added per path.The onboarding checkbox, annotated
chunks/welcome-Bn2nJPUT.js// welcome/App.vue (compiled) -- the onboarding screen's data-sharing checkbox.setup() { // Vue ref backing the checkbox. Starts checked (true) before any user // interaction; a stored value only overwrites it later if one already // exists in chrome.storage.local from a previous run. const dataCollectionEnabled = ref(true); onMounted(async () => { const stored = (await storage.local.get("dataCollectionEnabled"))["dataCollectionEnabled"]; if (typeof stored === "boolean") dataCollectionEnabled.value = stored; }); // Only called when the user finishes the welcome flow (Sign in or // Continue without signing in); writes whatever the checkbox currently // holds, which is `true` unless the user unchecked it first. async function persistDataCollectionConsent() { await storage.local.set("dataCollectionEnabled", dataCollectionEnabled.value); }}// Template: a plain <input type="checkbox"> two-way-bound to that ref, with// no default `checked=false` override.<input id="welcome-data-sharing" type="checkbox" v-model="dataCollectionEnabled" />When this fires
Every 5 minutes
A scheduled alarm fires on a fixed interval and uploads everything queued since the last run.
Where the browsing records go
- webguardian.st-panel-api.com
Sensor Tower's panel-api backend for this extension, shared with its StayFree/StayFocusd apps; receives session and page-view uploads keyed to your install ID.
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.
Ad-finder SDK reads sponsored listings and your search term on 3 retailers
A bundled ad-measurement module reads sponsored product listings plus your search term on Walmart, Target, and Instacart, then uploads them to a Sensor Tower endpoint every 3 seconds while the results page stays open.
What actually happens
You search for a common product on Walmart, Target, or Instacart and sponsored listings render on the results page.
A bundled ad-measurement module reads each sponsored listing's title, price, and size along with your search term, then uploads them to a Sensor Tower endpoint for that retailer.
The scan reruns every 3 seconds for as long as the results page stays open.
Fields captured per sponsored ad
- Ad titleBounty Paper Towels, 12 Double Rolls
The text of the sponsored product listing shown to you.
- Price and size$18.97, 12 ct
The listed price and package size of that sponsored product.
- Your search termpaper towels
What you typed into the retailer's search box to bring up those ads.
- Install ID8f2e1a90-6c4d-4b3f-9a21-77e0c4d8b615
Ties this shopping activity back to the same you across sessions.
The code that does this
The Walmart ad scanner and its 3-second upload loop
pd(), annotated
content-scripts/usage-helper.js// pd(container, searchUrl, onAdFound) -- content-scripts/usage-helper.js.// Runs against document.body on www.walmart.com search-results pages.function findWalmartSponsoredAds(container, searchUrl, onAdFound) { const searchInput = container.querySelector('input[name="q"]'); const found = []; const timestamp = Math.round(Date.now() / MS_PER_SEC); container.querySelectorAll('div[data-item-id], li[data-item-id]').forEach((card) => { if (!card?.textContent?.includes("Sponsored")) return; // only sponsored cards const titleEl = card.querySelector('[data-automation-id="product-title"]'); const priceEl = card.querySelector('div[data-automation-id="product-price"]'); let price = Array.from(priceEl?.querySelectorAll("span") || []) .find((s) => s.textContent?.includes("current price")) ?.textContent?.trim(); // strip "current price " / "Now " / "Was " label prefixes for (const prefix of ["current price ", "Now ", "Was "]) { if (price?.startsWith(prefix)) price = price.replace(prefix, "").split(",")[0].trim(); } const brand = titleEl?.parentElement?.previousElementSibling ?.querySelector("div.mv1")?.textContent?.trim(); const title = titleEl?.textContent?.trim(); const ad = { title: brand && title && !title.includes(brand) ? `${brand} ${title}` : title || "", size: priceEl?.querySelector("div:last-child")?.textContent || "", price, timestamp, store: "", search_url: searchUrl, search_term: searchInput?.value || "", // your typed search term }; if (ad.title) { found.push(ad); onAdFound?.(ad, card); } }); return found;}Vu()/Hu(), annotated
content-scripts/usage-helper.js// Vu(config) -- registers one retailer's poll loop; called once per site// (walmart/target/instacart) when the current page URL matches config.url.function registerAdFinderLoop(config) { const currentUrl = window.location.href; if (!currentUrl.includes(config.url)) return; markUrlAsCaptured(currentUrl); const alreadyUploaded = []; const inFlight = new Set(); setInterval(async () => { let ads = config.finder(document.body, currentUrl, config.onAdFound, config.instacartSelectors); ads = ads.filter((ad) => !alreadyUploaded.includes(ad.title) && !inFlight.has(ad.title)); ads.forEach((ad) => inFlight.add(ad.title)); try { await uploadNewAds(config, ads); ads.forEach((ad) => alreadyUploaded.push(ad.title)); } catch {} finally { ads.forEach((ad) => inFlight.delete(ad.title)); } }, config.loadDelay || 3000); // default: every 3 seconds}// uploadNewAds -- forwards the batch to the background service worker's// panel-api client, which POSTs to /v1/desktop/retail/{website}.async function uploadNewAds(config, ads) { if (ads.length === 0) return; await config.api.uploadRetailAds({ appId: config.appId, installId: await config.installId, website: config.website, ads, });}Where scraped ad and search data go
- webguardian.st-panel-api.com
Sensor Tower's panel-api backend for this extension; receives the scraped ad and search-term records keyed to your install ID.
- p.qljx.co
A Pathmatics/Sensor Tower crawl-upload endpoint the bundled ad-crawler module calls, enabled regardless of the data-collection setting.
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.
Sign in with Google asks for Gmail read access, then talks to a billing API
Signing in with Google from Settings or the welcome screen requests the gmail.readonly scope, unrelated to the site-safety purpose the extension advertises.
The token is then attached to requests made to purchases.stayfreeapps.com.
What actually happens
You open the extension's Settings or the post-install welcome tab and click Sign in with Google.
The consent screen names the requested scope before you approve it.
The extension requests the gmail.readonly scope, unrelated to its advertised purpose, then uses the resulting session for requests to its own billing API.
The Google access token is attached as a Bearer header on requests to purchases.stayfreeapps.com.
What the sign-in flow requests and where it lands
- OAuth scope requestedhttps://www.googleapis.com/auth/gmail.readonly
Grants read access to your Gmail inbox, a capability the extension's stated purpose has no use for.
- Access tokenya29.a0AfH6SMC7z9pQ1k...
The Google session credential this grant produces, kept by the extension and reused on later requests.
- Where the token is usedAuthorization: Bearer <token> to purchases.stayfreeapps.com
Requests carrying that token as a Bearer header go to the extension vendor's own billing backend, not Google's Gmail API.
- Internal account app idphone-guardian
Ties your Google sign-in to this extension's own account system.
The code that does this
Requesting the Gmail scope, then wiring the resulting token into the billing client
performLogin, annotated
background.js// performLogin -- background.js. Opens Google's OAuth consent screen and// pulls the authorization code back out of the redirect.function performLogin(env) { return async () => { const redirectUri = env.browser.identity.getRedirectURL(); const authUrl = buildAuthUrl({ clientId: env.clientId, redirectUri, scopes: env.scopes, // ["https://www.googleapis.com/auth/gmail.readonly"] }); const responseUrl = await env.browser.identity.launchWebAuthFlow({ url: authUrl, interactive: true, }); if (!responseUrl) throw new Error("No response from auth flow"); const params = new URL(responseUrl).searchParams; if (params.get("error")) throw new Error(params.get("error")); const code = params.get("code"); if (!code) throw new Error("No authorization code in response"); return { code, redirectUri }; };}Session config and Bearer-token fetch wrapper, annotated
background.jsconst GOOGLE_CLIENT_ID = "812707853907-9o271c7fbg18t067pdt8eodp5noed3tt.apps.googleusercontent.com";const authSession = createAuthSession({ storage: chromeStorageBackedStore, // keys: wg_auth_session, wg_auth_tokens installId: getInstallId(), config: { app: "phone-guardian", platform: "web-extension", clientId: `${runtime.id}/${version}` }, performLogin: performLogin({ browser: chrome, clientId: GOOGLE_CLIENT_ID, scopes: ["https://www.googleapis.com/auth/gmail.readonly"], }),});// Every request through this wrapper gets the signed-in session's Google// access token attached as a Bearer header, refreshing it once on a 401.async function authenticatedFetch(request, init) { await authSession.restoreSession(); const attach = async (token) => { const headers = new Headers(init?.headers); headers.set("Authorization", `Bearer ${token}`); return fetch(request, { ...init, headers }); }; let token = authSession.getState().tokens?.accessToken ?? (await authSession.refresh())?.accessToken; if (!token) throw new Error("not_signed_in"); const response = await attach(token); if (response.status !== 401) return response; const refreshed = await authSession.refresh(); if (!refreshed?.accessToken) throw new Error("session_expired"); return attach(refreshed.accessToken);}// Registers authenticatedFetch as the HTTP client for the purchases API.registerApiClient({ app: "phone-guardian", platform: "web-extension", clientId: `${runtime.id}/${version}`, baseUrl: "https://purchases.stayfreeapps.com", fetch: authenticatedFetch,});Where the sign-in flow sends requests
- accounts.google.com
Google's own OAuth consent endpoint; issues the authorization code for the gmail.readonly-scoped session.
- purchases.stayfreeapps.com
The extension vendor's own billing/account backend; receives Bearer-authenticated requests using the Google OAuth session from this sign-in.
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.
Where it sends data
Destinations our analysis observed Web Guardian: Phishing & Malware Protection 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.
- p.qljx.co
Web Guardian: Phishing & Malware Protection sends data to p.qljx.co. One other extension we have analyzed sends data here.
- purchases.stayfreeapps.com
Web Guardian: Phishing & Malware Protection sends data to purchases.stayfreeapps.com. One other extension we have analyzed sends data here.
- webguardian.st-panel-api.com
Web Guardian: Phishing & Malware Protection sends data to webguardian.st-panel-api.com. No other extension we have analyzed sends data here.
- Google (gmail.readonly OAuth scope)
Web Guardian: Phishing & Malware Protection sends data to Google (gmail.readonly OAuth scope). Named as a recipient in this extension's own analysis.
