Is WhatRuns safe?

High risk

WhatRuns is high risk. Every time you navigate to a new site, WhatRuns sends its hostname and URL to whatruns.com/api/v1/get_site_apps, no prompt or opt-out. If logged in, email and API key go too, base64-encoded. Confirmed by five captured POSTs.…

https://whatruns.comv1.10.0Chrome Web Store
77Risk
Who publishes it

Ownedit Ltd - no other listings under this identity, 2 shared hostnames

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

Publisher
https://whatruns.com
Declared legal entity
Ownedit Ltd
Registered address
11 Brindley Place, Birmingham B1 2LP, GB
Registered contact
WhatRuns

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

cdn.whatruns.com
Also called by 2 other listings: WhatRuns, Lookup - Website Profiler
whatruns.com
Also called by 3 other listings, including WhatRuns

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

Publishers can request a review.

Findings

Every site you visit is reported to WhatRuns servers

Every time you navigate to a new site, WhatRuns sends its hostname and URL to whatruns.com/api/v1/get_site_apps, no prompt or opt-out.

If logged in, email and API key go too, base64-encoded.

Confirmed by five captured POSTs.

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

You navigate to any website in your browser.

The extension did this

WhatRuns immediately sends that site's hostname and URL to its own servers, before you have finished loading the page.

This fires on every main_frame navigation, with no domain allowlist and no user prompt.

Captured request
POSThttps://www.whatruns.com/api/v1/get_site_apps

HTTP 200. Response includes technology fingerprint JSON and optional dom_data config object used to configure DOM scraping on subsequent page loads.

Headers
Content-Type
application/x-www-form-urlencoded
Body
data=%7B%22rawhostname%22%3A%22github.com%22%2C%22hostname%22%3A%22github.com%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2F%22%2C%22encode%22%3Atrue%7D
What WhatRuns sends on every page navigation
  • The site you are visiting
    https://github.com/

    The full URL of the page you just loaded, including any query parameters.

  • Raw hostname
    github.com

    The exact hostname, including any subdomain, used to identify the specific site.

  • Root hostname
    github.com

    The main domain without subdomain (e.g. 'github.com' instead of 'gist.github.com').

  • Your email address (if logged in)
    dXNlckBleGFtcGxlLmNvbQ==

    Your WhatRuns account email, base64-encoded and included in every request if you have ever signed in.

  • Your API key (if logged in)
    YWJjMTIzZGVmNDU2

    Your WhatRuns API key, base64-encoded. Uniquely identifies your account on every request.

The code that does this

The listener that fires on every navigation, from background.js

Readable version

What the navigation listener does

// Fires every time any page finishes loading in any tab.browser.webRequest.onCompleted.addListener(function(details) {  const url = normaliseURL(details.url);  const rawHostname = extractHostname(url);   // e.g. 'gist.github.com'  sendURLToWhatRunsServer(details.tabId, rawHostname, url);}, {  urls: ['http://*/*', 'https://*/*'],  // every HTTP/HTTPS URL  types: ['main_frame']                  // top-level navigations only});

How your URL is transmitted

function sendURLToWhatRunsServer(tabId, rawHostname, url) {  // Build the payload with hostname + URL  const payload = buildUrlPayload(url, rawHostname);  // Attach your account email + API key if you're logged in (base64-encoded)  const payloadWithAuth = attachCredentials(payload);  // POST it to WhatRuns — unconditionally, on every navigation  fetch('https://www.whatruns.com/api/v1/get_site_apps', {    method: 'POST',    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },    body: 'data=' + encodeURIComponent(JSON.stringify(payloadWithAuth))  });}
Where your browsing data is sent
    • www.whatruns.com

    Primary server receiving every navigation event, owned by WhatRuns. Responds with technology-detection data and, for targeted sites, DOM scraping configuration objects.

WhatRuns posts page URL, title, referrer, and UUID

We observed WhatRuns POST to whatruns.com/api/v1/collect_data with the page URL, title, referrer, a UUID, and version.

In shipped v1.10.0, the flow builds these after a two-second timer, posting to api/v1/analyse/path.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You open a web page while WhatRuns is installed.

The content script runs on http and https pages at document idle.

The extension did this

The extension builds a page record and sends it to a WhatRuns API endpoint.

The record includes the page URL, title, referrer, a stored UUID, and the extension version.

Fields observed in the page-load POST
  • Current page URL
    https://www.amazon.com/

    Shows the exact page you were viewing when the extension sent the request.

  • Page title
    Amazon.com. Spend less. Smile more.

    Adds readable context about the page you visited, even when the URL alone is not descriptive.

  • Referrer
    https://www.amazon.com/

    Shows the page that led you to the current page when the browser provides one.

  • Stored UUID
    3a4659a6dc0c468680cd66d0261bd6b0

    Lets repeated page reports from the same browser installation be linked together over time.

  • Extension version
    1.8.20

    Identifies which WhatRuns build produced the report.

Captured request
POSThttps://www.whatruns.com/api/v1/collect_data
Body
{  "url": "https://www.amazon.com/",  "title": "Amazon.com. Spend less. Smile more.",  "referrer": "https://www.amazon.com/",  "uuid": "3a4659a6dc0c468680cd66d0261bd6b0",  "plugin_version": "1.8.20"}
The code that does this

The shipped code builds the page record and posts it to WhatRuns

Readable version

Content script waits two seconds, then sends page fields

js/content.js
primeFlow: async function() {  try {    var t = {      id: FLOW_BOOT_SIGNAL    };    let e = this;    setTimeout(function() {      e.pushRuntimeMessage(t, t => {})    }, 2e3)  } catch (t) {    console.log("content [primeFlow] error: ", t)  }}flushBeacon: async function() {  try {    const e = {      url: window.location.href,      title: document ? document.title : "",      referrer: document.referrer || "",      uuid: await this.ensureRuntimeKey(),      plugin_version: chrome.runtime.getManifest().version    };    var t = {      id: FLOW_ECHO_SIGNAL,      data: e    };    this.pushRuntimeMessage(t, t => {})  } catch (t) {    console.log(t)  }}ensureRuntimeKey: async function() {  let t = await this.readStoreValue("wrs_session_uuid");  if (t) return t;  let e = this.buildRuntimeKey();  return this.writeStoreValue("wrs_session_uuid", e), e}buildRuntimeKey: function() {  const t = new Uint8Array(16);  return crypto.getRandomValues(t), t[6] = 15 & t[6] | 64, t[8] = 63 & t[8] | 128, [...t].map((t, e) => "" + t.toString(16).padStart(2, "0")).join("")}

Background worker serializes the data into a POST body

js/background.js
forwardRuntimeEcho: async function(t) {  try {    await _this.makeRequest({      type: "POST",      url: ANALYSE_PATH,      reqBody: t    })  } catch (t) {    console.log(t)  }}activateRuntimeBridge: async function(t, e) {  BROWSER.tabs.sendMessage(e, {    id: FLOW_PRIME_SIGNAL,    data: {}  }, function(t) {    return !0  })}makeRequest: async function(t) {  let {    type: e = "GET",    url: a,    headers: o = {},    reqBody: n = {}  } = t, s = {    status: 599,    msg: "Error"  };  try {    "Content-Type" in o || (o["Content-Type"] = "application/json");    let t = {      method: e,      body: JSON.stringify(n),      headers: o    };    "GET" === e && delete t.body, await fetch(a, t).then(t => t.json()).then(t => {      s = t    }).catch(t => {      console.log(t), s.msg = t    })  } catch (t) {    return console.log(3), void(s.msg = t.toString())  }  return s}

Constant resolving the current v1.10.0 destination

js/global.constants.js
export const DOMAIN_NAME = "https://www.whatruns.com/";export const CDN_DOMAIN_NAME = "https://cdn.whatruns.com/";export const BROWSER = chrome || browser;export const ANALYSE_APPS = "analyseApps";export const GET_DETECTED_APPS = "getDetectedApps";export const GET_NOTIFICATION_MESSAGE = "getNotificationMessage";export const GET_HOST_NAME = "getHostName";export const SET_DATA = "setData";export const GET_DATA = "getData";export const GET_TECHS = "get_techs";export const GET_SITE_DATA = "get_site_data";export const KEY_DETAILS = "keyDetails";export const FORM = "form";export const GET_SITE_APPS = DOMAIN_NAME + "api/v1/get_site_apps";export const GET_SITE_APPS_BY_DATA = DOMAIN_NAME + "api/v1/get_site_apps_by_data";export const ANALYSE_EMAILS = DOMAIN_NAME + "api/v1/analyse_emails";export const REVIEW_FEATURE_DATA = DOMAIN_NAME + "api/v1/ext_review";export const invalidDomains = ["localhost", "127.0.0.1", "0.0.0.0"];export const NO_APPS_FOUND = " I feel lost, maybe there's nothing to be found ; ) ";export const ANALYSE_PAGE = DOMAIN_NAME + "api/v1/analyse/page";export const ANALYSE_PATH = DOMAIN_NAME + "api/v1/analyse/path";
Network destination for the page record
    • www.whatruns.com

    Receives the page-load record containing URL, title, referrer, stored UUID, and extension version.

Navigation beacon sends URL, title, referrer, and persistent ID to whatruns.com

Dynamic analysis captured POSTs to whatruns.com/api/v1/analyse/path on every navigation: URL, title, referrer, and a persistent UUID from storage.local.

The UUID recurred on unrelated sites (facebook, github, wikipedia), linking history.

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

You visit any webpage.

The content script runs on every page due to the extension's <all_urls> match pattern.

The extension did this

The extension sends your page URL, title, referrer, and a persistent tracking ID to whatruns.com.

Two seconds after page load, background.js POSTs this data to https://www.whatruns.com/api/v1/analyse/path. The same UUID persists across all sites, linking visits into a cross-site browsing history.

Captured request
POSThttps://www.whatruns.com/api/v1/analyse/path

HTTP 200. Same UUID observed across 7+ navigations to unrelated domains in our test session (observed during dynamic analysis).

Headers
Content-Type
application/json
Body
{  "url": "https://github.com/torvalds/linux",  "title": "torvalds/linux: Linux kernel source tree",  "referrer": "https://github.com",  "uuid": "d45bfbf41ccd49899dddebba958c111b",  "plugin_version": "1.10.0"}
Fields transmitted in every beacon POST
  • Page URL
    https://github.com/torvalds/linux

    The full address of every page you visit, including paths and query parameters.

  • Page title
    torvalds/linux: Linux kernel source tree

    The document title of the page, often revealing the topic or content you were viewing.

  • Referrer
    https://github.com

    The URL of the page you came from, revealing navigation patterns and sequences.

  • Persistent UUID
    d45bfbf41ccd49899dddebba958c111b

    A randomly generated ID stored permanently in your browser, sent on every page load, letting the server link your visits into one history.

  • Extension version
    1.10.0

    The version of the WhatRuns extension installed.

What's stored on your device

Written once, never rotated; read at the start of every beacon call, so the same value is sent each session until uninstall or manual clear.

Location
chrome.storage.local key 'wrs_session_uuid'
Contents
{  "wrs_session_uuid": "d45bfbf41ccd49899dddebba958c111b"}
The code that does this

Beacon assembly and dispatch, content.js (shipped vs deobfuscated)

Readable version

flushBeacon and primeFlow (deobfuscated, from deobfuscated/js/content.js)

js/content.js
flushBeacon: async function() {  try {    const e = {      url: window.location.href,      title: document ? document.title : "",      referrer: document.referrer || "",      uuid: await this.ensureRuntimeKey(),      plugin_version: chrome.runtime.getManifest().version    };    var t = {      id: FLOW_ECHO_SIGNAL,      data: e    };    this.pushRuntimeMessage(t, t => {})  } catch (t) {    console.log(t)  }},primeFlow: async function() {  try {    var t = {      id: FLOW_BOOT_SIGNAL    };    let e = this;    setTimeout(function() {      e.pushRuntimeMessage(t, t => {})    }, 2e3)  } catch (t) {    console.log("content [primeFlow] error: ", t)  }},ensureRuntimeKey: async function() {  let t = await this.readStoreValue("wrs_session_uuid");  if (t) return t;  let e = this.buildRuntimeKey();  return this.writeStoreValue("wrs_session_uuid", e), e},buildRuntimeKey: function() {  const t = new Uint8Array(16);  return crypto.getRandomValues(t), t[6] = 15 & t[6] | 64, t[8] = 63 & t[8] | 128, [...t].map((t, e) => "" + t.toString(16).padStart(2, "0")).join("")}
Beacon destination
    • www.whatruns.com

    Receives per-navigation POST beacons with URL, title, referrer, and persistent UUID. Operated by WhatRuns. /api/v1/analyse/path isn't documented in the listing or policy.

Our write-ups

Updated 30 September 2026cmkdbmfndkfgebldhnkbfhlneefdaaip