Is Web Guardian: Phishing & Malware Protection safe?

Medium risk

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).

ST Advancedv0.5.2Chrome Web Store
58Risk
Who publishes it

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.

Publisher
ST Advanced
Declared legal entity
SensorTower Inc.

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.

adblockluna.com
Also called by 1 other listing: Luna Adblock for Youtube & Websites
api.stayfreeapps.com
Also called by 3 other listings, including StayFree - Web Analytics & Time Tracker
p.qljx.co
Also called by 3 other listings, including Luna Adblock for Youtube & Websites
purchases.stayfreeapps.com
Also called by 5 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

Web Guardian logs every site you visit, with collection on by default

HighunwantedCWE-359Static analysis

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.

What actually happens

You did this

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 extension did this

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 ID
    8f2e1a90-6c4d-4b3f-9a21-77e0c4d8b615

    A persistent identifier that ties every site you visit, across every session, back to the same you.

  • Website visited
    www.chase.com

    The hostname of each site you browse to, logged for every page you view.

  • Session duration
    182 seconds, 2026-09-18T14:02:11Z

    How long you stayed on that site, with a timestamp.

  • Referrer and campaign tags
    utm_source=newsletter&utm_campaign=fall_sale

    Where you came from and any UTM marketing tags on the link you followed.

  • Birth year
    1991

    An optional age indicator you can give during onboarding.

The code that does this

The upload function and the checkbox that gates it

Readable version

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

HighunwantedCWE-359Static analysis

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 did this

You search for a common product on Walmart, Target, or Instacart and sponsored listings render on the results page.

The extension did this

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 title
    Bounty 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 term
    paper towels

    What you typed into the retailer's search box to bring up those ads.

  • Install ID
    8f2e1a90-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

Readable version

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

HighunwantedCWE-522Static analysis

Signing in with Google from Settings or the welcome screen requests the gmail.readonly scope, unrelated to the site-safety purpose the extension advertises.

What actually happens

You did this

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 did this

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 requested
    https://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 token
    ya29.a0AfH6SMC7z9pQ1k...

    The Google session credential this grant produces, kept by the extension and reused on later requests.

  • Where the token is used
    Authorization: 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 id
    phone-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

Readable version

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.js
const 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.

Updated 2 November 2026aajmomfdmgpnkhoekokehfgablakcofi