Is Molly safe?

Low risk

Molly sends visited URLs and a device fingerprint to api.molly.gratis and injects the user's JWT token into pages on partner sites.

The extension records every URL the user navigates to and periodically transmits them to api.molly.gratis/updatevisits. On each session start it also sends OS type, architecture, and a persistent install UUID. When a user visits bownty.dk, all2day.dk, or mollyapp.io, the extension injects the user's JWT authentication token into the page DOM as a data attribute, where any script running on those pages can read it.

Mollyv1.15.1Chrome Web Store
31Risk
Who publishes it

Bownty ApS - no other listings under this identity, 3 shared hostnames

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

Publisher
Molly
Declared legal entity
Bownty ApS
Registered address
Lautruphøj 5, Ballerup 2750, DK
Registered contact
Bownty ApS

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

api.pricerunner.com
Also called by 2 other listings, including Karma | Online shopping, but better
de.trustpilot.com
Also called by 3 other listings, including iGraal, iGraal - Cashback & codes promo
nl.trustpilot.com
Also called by 3 other listings, including ClockAssist

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

Publishers can request a review.

Findings

Full browsing history sent to Molly's servers on every page visit

Every page you visit, search and product queries included, is sent as a full URL to api.molly.gratis/updatevisits.

Seen for a Google search, a Wikipedia article, a store page.

Only token-like segments are redacted; search terms pass.

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

You navigate to a new page, or switch to a different tab.

Any http/https navigation fires chrome.tabs.onUpdated or chrome.tabs.onActivated.

The extension did this

The extension records the full page URL and queues it to be sent to Molly's server.

registerPageView() stores {url, timestamp, tab_id} and schedules a batched POST to api.molly.gratis/updatevisits.

The code that does this

registerPageView() and reduceUrl(), capturing and lightly redacting the visited URL

Readable version

registerPageView — records every navigated URL

background.js
registerPageView(url, tabId) {  const entry = {    url: url = Visit.reduceUrl(url),    ts: +new Date,    tab_id: tabId  };  this._data.page_views = this._data.page_views || [];  this._data.page_views.push(entry);  this.end = +new Date;  Visit.registerChange(this);}

reduceUrl — replaces UUID-like/high-entropy path and query segments with [id], leaves plain words alone

background.js
static reduceUrl(url) {  function reduceSegment(segment) {    segment = decodeURIComponent(segment);    if (!segment.length) return '';    if (segment.length < 5) return segment;              // short strings pass through    if (segment.match(/.*@.*\.[a-z]+/)) return '[email]';    if (segment.match(/^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}/)) return '[id]'; // UUID    if (segment.match(/^https?:\/\//)) return '[url]';    // entropy check: mostly-alphabetic real words score low and are returned unchanged    let bits = 0, count = 0, prev = '^';    for (const ch of segment) {      const c = /[0-9]/.test(ch) ? '0' : ch;      if (/[a-zæøå]/i.test(c)) { bits += TRANSITION_COST[prev + c] || 18; count += 10; prev = c; }      else prev = undefined;    }    return Math.exp(count - bits) < 1 ? '[id]' : segment; // low-entropy word -> kept as-is  }  const u = new URL(url);  u.pathname = u.pathname.split(/(\/)/).map(seg => seg.match(/^(|c-\d+)$/) ? seg : reduceSegment(seg)).join('');  for (const key of u.searchParams.keys()) {    if (/variant|product/.test(key)) continue;    u.searchParams.set(key, /id|token|signature/.test(key) ? '[id]' : reduceSegment(u.searchParams.get(key)));  }  return u.toString();}

updateVisits — sends the queued visit records

background.js
static async updateVisits(changes) {  return await this.api('updatevisits', {}, changes);}
Captured request
POSThttps://api.molly.gratis/updatevisits
Body
{  "landingpage": "https://www.google.com/search?q=laptop"}
Fields carried in the updatevisits request body
  • Page URL
    https://www.google.com/search?q=laptop

    The full address of the page you're viewing, including path and most query-string values.

  • Site domain
    google.com

    The domain of the site you're visiting, used to group your page views by website.

  • Browser tab ID
    874

    An internal tab identifier used to group page views from the same browsing session together.

  • Visit timestamp
    1755307860130

    The exact time each page view was recorded, in milliseconds since epoch.

When this fires

When a batch threshold is hit · flushes at 10 queued events

Page-view changes queue in memory and flush to the server once either 60 seconds have passed since the first queued change or 10 pages have accumulated, whichever comes first; a 30-second background alarm acts as a backup flush if the service worker was asleep.

Destination receiving the visited-page URLs
    • api.molly.gratis

    Molly's own visit-tracking API. Receives the lightly-redacted URL of every page you visit, along with tab and timing metadata, via the /updatevisits endpoint.

Search result domains sent to Molly's coupon API on every query

On Google Search or PriceRunner results pages, Molly's content script collects every result link's domain.

The background script checks each against Molly's site-info API for coupons, revealing your search categories and retailers to Molly.

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

You load a Google Search or PriceRunner results page.

The tab URL is matched against /^google\.(dk|se|com)|pricerunner.com/i on every navigation event.

The extension did this

The extension collects every result link's destination domain and queries Molly's coupon API for each one.

linkSearch() calls GET molly.gratis/api/site/{domain} for each distinct retailer domain found in the search results, authenticated with your JWT token.

The code that does this

Search result domain collection and API lookup

Readable version

searchenhancer.js — collects result URLs and sends them to background

searchenhancer/searchenhancer.js
async update() {  const linkElements = platform.findLinks();  const urls = linkElements.map(el => el.url).filter(Boolean);  if (!urls.length) return;  const siteInfo = await new Promise(resolve => {    chrome.runtime.sendMessage({ action: 'enhancer_init', links: urls }, resolve);  });}

background.js — linkSearch extracts domains and looks each one up

background.js
static async linkSearch(urls, noCache) {  const cached = {};  const toFetch = {}; // domain -> [urls]  for (const url of urls) {    const domain = Site.getDomainFromUrl(url);    const cached_site = await Site.getFromDomain(domain, { no_cache: noCache, no_fetch: true });    if (cached_site) {      cached[url] = cached_site;    } else {      toFetch[domain] = [...(toFetch[domain] || []), url];    }  }  await Promise.all(Object.keys(toFetch).map(async domain => {    const site = await Site.getFromDomain(domain, { no_cache: noCache, timeout: 5000 });    if (site) toFetch[domain].forEach(url => { cached[url] = site; });  }));  return cached;}

background.js — getFromDomain fetches molly.gratis/api/site/{domain} with JWT auth

background.js
const apiUrl = config.site_info_url + '/' + encodeURIComponent(domain);const response = await fetch(apiUrl, {  headers: { Authorization: 'basic ' + await Settings.getToken() }});
Information inferred from the domain lookup pattern
  • Retailer domains in your search results
    amazon.de, zalando.dk, elgiganten.dk

    Each result-link domain is checked against Molly's database, revealing which product categories and retailers you searched.

  • JWT token (sent in lookup request)
    eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiNjM4YTFiMmMifQ.sig

    Your Molly session token is sent in the Authorization header of every domain lookup, linking the request to your account.

When this fires

On an interval

Re-triggered by MutationObserver on any DOM change on the search results page (searchenhancer.js:8210), so lazy-loaded or dynamically added results trigger additional domain lookups without a new page navigation.

Destination receiving the domain lookup requests
    • molly.gratis

    Molly's site-info and coupon API. Receives GET /api/site/{domain} for every retailer domain appearing in your search results, authenticated with your JWT token.

Persistent device fingerprint sent to Molly on every session update

Each session start assembles a device profile, OS, CPU architecture, browser type, and a permanent random UUID, attached to every session-update request to api.molly.gratis.

The UUID persists across restarts, correlating activity over time.

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

You open a new browser session or navigate to a new tab.

Any browser startup or tab activity that triggers a session update.

The extension did this

The extension reads your OS type, CPU architecture, and a stored permanent ID, then sends them to api.molly.gratis.

The device_info object is attached to the updatesession POST body on every call.

Fields sent in the device_info object with every session update
  • Permanent install ID
    f47ac10b-58cc-4372-a567-0e02b2c3d479

    A UUID generated once on install and stored permanently, tying every session update to you even after restarts.

  • Operating system
    mac

    The OS platform returned by chrome.runtime.getPlatformInfo (e.g. mac, win, linux).

  • CPU architecture
    x86-64

    The processor architecture of your machine (e.g. x86-64, arm).

  • Notification status
    default

    Whether you have granted notification permission to the extension (default, granted, or denied).

The code that does this

getDeviceInfo() and updateSession(), device profile assembly and transmission

Readable version

getDeviceInfo — assembles the device profile

background.js
static async getDeviceInfo(forceRefresh) {  if (this._deviceInfo === undefined || forceRefresh) {    // Try native API first, fall back to storage    this._deviceInfo = await storage.get('_deviceInfo') || {};    this._deviceInfo.notification_status = this._deviceInfo.notification_status || 'default';    const platformInfo = await chrome.runtime.getPlatformInfo();    this._deviceInfo.platform = platformInfo.os;    let { install_id } = await storage.getMulti(['install_id']) || {};    if (!install_id) {      install_id = crypto.randomUUID();      storage.set('install_id', install_id);    }    this._deviceInfo.install_id = install_id;  }  return this._deviceInfo;}

updateSession — attaches device_info to every session POST

background.js
static async updateSession(sessionData) {  const deviceInfo = await this.getDeviceInfo();  sessionData.device_info = deviceInfo;  return await this.api('updatesession', {}, sessionData, { ... });}
Destination receiving the device fingerprint
    • api.molly.gratis

    Molly's backend API. Receives the full device_info object (install_id UUID, OS, arch, notification_status) with every session update.

What it can do

Permissions this extension asks for, as declared in version 1.14.1. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to. The current listed version is 1.15.1, which we have not unpacked yet.

  • Read and change your data on every site you visit

    http://*/*

  • Read and change your data on every secure site you visit

    https://*/*

  • See the address and title of every tab you have open

    tabs

  • Run its own code inside the pages you visit

    scripting

  • Store data in your browser

    storage

  • Schedule its own background tasks

    alarms

  • Run hidden pages in the background

    offscreen

Where it sends data

Destinations our analysis observed Molly 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.

  • api.molly.gratis

    Molly sends data to api.molly.gratis. No other extension we have analysed sends data here.

Updated 30 September 2026jcbjknkcpodniflhegplcmjonnfhnhmi