Is stopRU safe?

Medium risk

stopRU sends visited YouTube page URLs and detected video/channel details to Mixpanel analytics.

When stopRU runs on YouTube or YouTube Kids, it builds analytics events that include the current page URL, browser and extension details, and detected video or channel metadata such as names, IDs, and durations. Its background script forwards those events with a persistent user ID to Mixpanel's EU API.

Tarasv0.0.56Chrome Web Store
45Risk

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

Publishers can request a review.

Findings

YouTube page and video metadata sent to Mixpanel

stopRU POSTs a UserTrackDaily event to api-eu.mixpanel.com/track?ip=1 when YouTube loads, with the current URL, extension version, and a persistent distinct_id.

The same path adds video and channel fields on remove or report actions.

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

You load YouTube or YouTube Kids with the extension enabled.

The extension did this

The extension prepares a Mixpanel analytics event containing page context and, on media-removal paths, video or channel details.

Fields the code places in Mixpanel event properties
  • Current YouTube page
    https://www.youtube.com/

    Shows which YouTube or YouTube Kids page was open when the event was reported.

  • Extension version
    0.0.53

    Shows which installed build generated the event.

  • Persistent extension ID
    9b2f3a73-4e6e-4f33-b7c1-4d6c8b3e2291 (illustrative)

    Links events from the same browser installation over time.

  • Browser details
    browserName=Chrome; browserVersion=126 (illustrative)

    Adds context about the browser used when the YouTube page was open.

  • Video and channel details
    channelId=UC_x5XG1OV2P6uZZ5FSM9Ttw; videoId=dQw4w9WgXcQ; videoDuration=3:33 (illustrative)

    On removal and error paths, this can describe the YouTube item the extension acted on.

  • Request IP address
    Collected by Mixpanel because the URL includes ip=1

    The request URL asks Mixpanel to include the network IP address with the event.

Captured request
POSThttps://api-eu.mixpanel.com/track?ip=1
Headers
Accept
text/plain
Content-Type
application/json
The code that does this

The content script builds the properties and the background worker posts them to Mixpanel

Readable version

Daily YouTube event and property merge

deobfuscated/youtube.js
if (getCookie('stopru-mixpanel-tracked') != 'true') {    trackMixpanelEvent(        message='',        info=null,        stepLogs=null,        eventName='UserTrackDaily',    );    let exdexpireDate = new Date();    exdexpireDate.setTime(exdexpireDate.getTime() + (1 * 24 * 60 * 60 * 1000));    exdexpireDate.setHours(0, 0, 0, 0);    setCookie('stopru-mixpanel-tracked', 'true', exdexpireDate);}function trackInfo(message, info, stepLogs) {    trackMixpanelEvent(message, info, stepLogs, eventName='infoEvent');}function trackError(message, info, stepLogs) {    trackMixpanelEvent(message, info, stepLogs, eventName='errorEvent');}function trackMixpanelEvent(message, info, stepLogs, eventName) {    log('trackMixpanelEvent', eventName, message, info);    logObject(stepLogs);    let properties = {        message: message,        ...getEventMetaData(),        ...info,    };    if (stepLogs) {        properties['stepLogs'] = stepLogs;    }    chrome.runtime.sendMessage({        command: 'mixnel-track',        data: {            eventName: eventName,            properties: properties,        },    }, (response) => {        log('trackMixpanelEvent -> response', response);    });}

Removal path and media-field collection

deobfuscated/youtube.js
function clickOnMenu(element, popup, stepLogs, actionName, menuClickCounter) {try {    if (menuClickCounter < 0) {        trackError(            message='No elements in menu.',            info={                "popup": popup.innerHTML.slice(700, 1500),                "menuItems": getMenuItemsFromPopup(popup),            },            stepLogs=stepLogs,        );        return;    }    let menuItemElementList = getMenuItemElementList(popup);    if (menuItemElementList.length == 0) {        setTimeout(clickOnMenu, 50, element, popup, stepLogs, actionName, menuClickCounter - 1);        return;    }    let {menuItemNumber, actualActionName} = getMenuActionItemNumber(popup, actionName);        if (menuItemNumber < 0) {        return;    }    if (!element.classList.contains('stopru-video')) {        trackInfo(            message="Outdated video",            info={                ...getElementData(element),            },            stepLogs=stepLogs,        );        return;    }    if (checkElement(element) == false) {        trackError(            message="Is not Ru video #1",            info={                ...getElementData(element),            },            stepLogs=stepLogs,        );        return;    }    if (ytkMode) {        menuItemElementList[menuItemNumber].querySelector('tp-yt-paper-item').click();    } else {        menuItemElementList[menuItemNumber].click();    }        element.classList.remove("stopru-video");    stepLogs['stepMenuClick'] = getStepLog(stepName='stepMenuClick', stepCounter=menuClickCounter);    if (!ytkMode) {        // no cancel button in ytk        setTimeout(cancelButtonHandler, 10, element, stepLogs, 5);    }    let eventName = 'notSetEventName';    let commandName;    if (actualActionName == 'cancel-video') {  // not interested        eventName = 'videoRemoved';        commandName = 'create-video-blocked';    }    if (actualActionName == 'cancel-channel') {  // don't recommend channel        eventName = 'channelRemoved';        commandName = 'create-channel-blocked';    }    let postData = getElementData(element);    if (commandName) {        chrome.runtime.sendMessage({command: commandName, data: postData}, (response) => {            log('create item in DB -> response', response);        });    }    logObject(stepLogs);    chrome.runtime.sendMessage({        command: 'mixnel-track',        data: {            eventName: eventName,            properties: {...postData, ...getEventMetaData(), stepLogs},        },    }, (response) => {        log('mixnel-track -> response received', response);    });} catch(e) {    trackError(        message="clickOnMenu exception",        info={            ...getElementData(element),            error: e.stack,        },        stepLogs=stepLogs,    );    setTimeout(clickOnMenu, 50, element, popup, stepLogs, actionName, menuClickCounter - 1);    return;}}function getElementData(element) {    let videoData = getVideoData(element);    let channelData = getChannelData(element);    let videoDuration = getVideoDuration(element);    return {        elementName: getElementName(element),        channelId: channelData['channelId'],        channelName: channelData['channelName'],        videoId: videoData['videoId'],        videoName: videoData['videoName'],        videoNameOld: getVideoNameOld(element),        videoDuration: videoDuration,        videoDurationSeconds: convertDurationToSeconds(videoDuration),    }}

Page metadata function

deobfuscated/common.js
function getEventMetaData() {    return {        "url": window.location.href,        "version": chrome.runtime.getManifest().version,        "ytkMode": getYTKMode(),        ...getBrowserData(),    }}

Background Mixpanel POST branch

deobfuscated/background.js
if (msg.command == "mixnel-track") {    console.log('stopru: CMD, mixnel-track');    const options = {      method: 'POST',      headers: {Accept: 'text/plain', 'Content-Type': 'application/json'},      body: JSON.stringify([{        event: msg.data.eventName,        properties: {          token: 'c6eb98ba654092d547409812229b003f',           distinct_id: userId,          ...msg.data.properties,        },      }])    };    fetch('https://api-eu.mixpanel.com/track?ip=1', options)      .then(response => response.json())      .then(response => console.log('stopru: Mixpanel response: ', response))      .catch(err => console.error('stopru: Mixpanel error: ', err));    resp({      type: 'result',      status: 'success',      data: {},      request: msg,    });    return true;  }
External destination receiving the analytics event
    • api-eu.mixpanel.com

    Mixpanel EU analytics endpoint that receives the YouTube event, persistent distinct_id, current URL, extension version, and any media fields added by the caller.

What it can do

Permissions this extension asks for, as declared in version 0.0.53. 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 0.0.56, which we have not unpacked yet.

  • Store data in your browser

    storage

  • Store an unlimited amount of data in your browser

    unlimitedStorage

Where it sends data

Destinations our analysis observed stopRU 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-eu.mixpanel.com

    stopRU sends data to api-eu.mixpanel.com. 4 other extensions we have analysed send data here.

Updated 30 September 2026adjmjjpnbddnlcmcjmlipkbldndkhhaj