Is stopRU safe?
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.
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
You load YouTube or YouTube Kids with the extension enabled.
The extension prepares a Mixpanel analytics event containing page context and, on media-removal paths, video or channel details.
- Current YouTube pagehttps://www.youtube.com/
Shows which YouTube or YouTube Kids page was open when the event was reported.
- Extension version0.0.53
Shows which installed build generated the event.
- Persistent extension ID9b2f3a73-4e6e-4f33-b7c1-4d6c8b3e2291 (illustrative)
Links events from the same browser installation over time.
- Browser detailsbrowserName=Chrome; browserVersion=126 (illustrative)
Adds context about the browser used when the YouTube page was open.
- Video and channel detailschannelId=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 addressCollected by Mixpanel because the URL includes ip=1
The request URL asks Mixpanel to include the network IP address with the event.
- Accept
- text/plain
- Content-Type
- application/json
The content script builds the properties and the background worker posts them to Mixpanel
Daily YouTube event and property merge
deobfuscated/youtube.jsif (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.jsfunction 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.jsfunction getEventMetaData() { return { "url": window.location.href, "version": chrome.runtime.getManifest().version, "ytkMode": getYTKMode(), ...getBrowserData(), }}Background Mixpanel POST branch
deobfuscated/background.jsif (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; }- 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.