Is Ninja ASO - Best ASO Toolbox For Google Play safe?
Ninja ASO is medium risk. On a Google Play app page, Ninja ASO reads the package ID from the URL and sends it within 150ms to appbrain.com and appstorespy.com, no consent prompt. Appstorespy's request uses a spoofed User-Agent; results cache 12h.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
App ID Sent to Third-Party ASO Services on Google Play Visits
On a Google Play app page, Ninja ASO reads the package ID from the URL and sends it within 150ms to appbrain.com and appstorespy.com, no consent prompt.
Appstorespy's request uses a spoofed User-Agent; results cache 12h.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You open any app's page on Google Play, such as https://play.google.com/store/apps/details?id=com.whatsapp.
The extension reads the app's package ID from the URL and automatically sends it to appbrain.com and appstorespy.com within 150 milliseconds, with no prompt or opt-in.
The background worker fetches the full appbrain.com page for that app and POSTs a GraphQL query to appstorespy.com/graphql concurrently. The appstorespy request includes a hardcoded spoofed desktop Windows User-Agent string.
Returns ASO metadata: install count, feature flag, version, size, updated date, released date.
- accept
- */*
- user-agent
- Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36
- content-type
- application/json
- accept-language
- en-US,en;q=0.9
{ "operationName": "getAppById", "variables": { "id": "com.whatsapp", "store": "play" }, "query": "query getAppById($store: ID!, $id: ID!) {\n app(store: $store, id: $id) {\n installs\n feature\n version\n size\n updated\n released\n }\n }\n"}Returns an HTML page; the extension scrapes it for downloads, ranking, APK size, Android version, library count, maturity rating, change logs, and keywords.
Content script: URL check and scrape trigger
Readable equivalent
// Runs immediately when the content script loads on a Play Store app details page.if (document.location.href.startsWith('https://play.google.com/store/apps/details?id=')) { setTimeout(async () => { // Extract the app package ID from the URL query string. const packageName = new URL(location.href).searchParams.get('id'); // Send it to the background service worker for third-party lookup. await chrome.runtime.sendMessage({ type: 'scrape', packageName }); }, 150); // fires 150 ms after page load}Background service worker: concurrent appbrain + appstorespy fetch
appstorespy fetch function (ec)
// Sends the app package ID to appstorespy.com via a GraphQL query.// The User-Agent header is hardcoded and does not match the user's browser.async function fetchAppStoreSpy(packageName) { const headers = new Headers(); headers.append('accept', '*/*'); headers.append('accept-language', 'en-US,en;q=0.9'); headers.append('content-type', 'application/json'); // Hardcoded spoofed Windows desktop User-Agent headers.append('user-agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36'); const body = { operationName: 'getAppById', variables: { id: packageName, store: 'play' }, query: `query getAppById($store: ID!, $id: ID!) { app(store: $store, id: $id) { installs feature version size updated released }}` }; const response = await fetch('https://appstorespy.com/graphql', { method: 'POST', headers, body: JSON.stringify(body), redirect: 'follow' }); return response.json();}Parallel dispatch and 12-hour cache write (hc)
// On cache miss: fetch both sources in parallel, then store for 12 hours.const [appbrainResult, appstoreSpyResult] = await Promise.all([ fetchAppBrain(packageName), // zo(): GET appbrain.com/app/<packageName> fetchAppStoreSpy(packageName) // ec(): POST appstorespy.com/graphql]);const combined = { appbrain: appbrainResult, appstorespy: appstoreSpyResult };const expiry = Date.now() + 43_200_000; // 12 hours in ms (432e5)await chrome.storage.local.set({ [packageName]: { data: combined, timestamp: expiry } });- appstorespy.com
ASO analytics platform. Receives the app package ID via a POST GraphQL query (getAppById) and returns install count, feature flag, version, size, release and update dates.
- www.appbrain.com
Android app index. Receives the package ID in a GET request URL path and returns downloads, ranking, APK size, library list, change logs, and keyword data via HTML scraping.
The extension caches ASO data for each app you viewed for up to 12 hours, keyed by package ID, a local history of apps you researched.
- Location
- chrome.storage.local, key = <packageName> (e.g. 'com.whatsapp')
{ "com.whatsapp": { "data": { "appbrain": { "data": { "appData": { "size": "52 MB", "ranking": "#1 in Communication", "installs": "5,000,000,000+", "maturity": "Everyone", "androidVersion": "5.0+", "librariesCount": "47" } }, "error": null }, "appstorespy": { "data": { "size": "54525952", "feature": true, "updated": "2024-05-14", "version": "2.24.10.82", "installs": "5000000000", "released": "2009-11-10" }, "error": null } }, "timestamp": 1748900400000 }}What it can do
Permissions this extension asks for, as declared in version 1.4.0. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.
Read and change your data on www.appbrain.com
https://www.appbrain.com/app/*
Read and change your data on appstorespy.com
https://appstorespy.com/apps/play/*
Read and change your data on play-lh.googleusercontent.com
https://play-lh.googleusercontent.com/*
Store data in your browser
storage