Is ggsel Ханти: 1080p на Twitch, игры дешевле и Steam в рублях safe?

Medium risk

The extension sends Twitch browsing activity and a persistent user ID to both the operator's backend and Yandex Metrica on every channel visit.

Each time a user visits a Twitch channel, the extension forwards the channel URL, game title, stream session details, and a stable cross-session UUID to twitch.ggsel.net and to Yandex Metrica (mc.yandex.ru). The persistent user ID is generated on first install and stored in chrome.storage.local, allowing the operator to track the same user across sessions. The extension also polls the operator backend every three minutes and displays push notifications with operator-controlled titles, bodies, and click URLs.

ggselv1.0.17.1Chrome Web Store
45Risk

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

Publishers can request a review.

Findings

Twitch activity sent to ggsel.net events backend

MediumunwantedCWE-359Dynamic sandbox

The extension POSTed an event batch to twitch.ggsel.net/api/v1/events.

What actually happens

You did this

You browse Twitch or open the extension popup while the extension is installed.

The content script reads the current Twitch page, channel name, and stream-session state from the page.

The extension did this

The extension creates analytics events and sends them to the ggsel.net events backend.

Those events include persistent user and session identifiers that connect multiple actions to the same browser profile.

Captured request

POSThttps://twitch.ggsel.net/api/v1/events

Observed during dynamic analysis: the JSON event batch included a user ID beginning with ac443594, a session ID beginning with 74ed2e90, extension version 1.0.17.1, activity-marker events, and a popup-open event.

Headers
content-type
application/json

Fields attached to the ggsel.net event batch

  • Persistent user ID
    ac443594-8a1f-4d3b-9f4c-2b1d6e7c8a90

    Lets the backend connect events from the same browser profile across separate browsing sessions.

  • Browser-session ID
    74ed2e90-2a6d-46c1-9f9d-1e8f7c5b4a32

    Groups activity that happened during the same browser session.

  • Extension version
    1.0.17.1

    Shows which installed version produced the event.

  • Twitch page location
    https://www.twitch.tv/shroud

    Shows which Twitch page or channel was open when the extension created the event.

  • Channel and game context
    channel_name=shroud, game_title=VALORANT

    Describes the stream channel and game associated with the visit or stream session.

  • Stream duration
    duration_sec=1840

    Records how long a stream session lasted before the tab closed, changed, or became idle.

The code that does this

Source code path that creates Twitch events and posts them to ggsel.net

Readable version

Endpoint configuration

common/config.js
export const DEFAULT_API_BASE = 'https://twitch.ggsel.net';export const REQUEST_TIMEOUT_MS = 7000;// Canonical source marker for links opened from the Twitch extension.export const DEFAULT_UTM_SOURCE = 'ggsel_twitch_extension';// Stable canonical UTM medium for the extension (spec 3.3)export const DEFAULT_UTM_MEDIUM = 'twitch_app';export const GA_MEASUREMENT_ID = 'G-NCQMPNWKWS';export const GA_API_SECRET = 'grdZVJk4ROmZPlW9HA_CpQ';export const YM_COUNTER_ID = '104649646';// Phase 1 own analytics — third sink parallel to GA4/Yandex.// Set to false as kill switch if backend is overloaded or under attack.export const OWN_ANALYTICS_ENABLED = true;export const OWN_ANALYTICS_TIMEOUT_MS = 5000;export function buildEventsEndpoint(base = DEFAULT_API_BASE) {  return new URL('/api/v1/events', base).toString();}

Twitch visit and stream-session events

content/twitch.content.js
  function trackVisit(reason) {    try {      const pageUrl = getSafePageLocation();      usageTracker?.notifyContextChange(buildUsageContext(), reason || 'visit');      if (pageUrl === lastVisitLocation) return;      lastVisitLocation = pageUrl;      const channelName = getChannelName();      sendAnalyticsEvent('twitch_visit', {        visit_reason: reason,        channel_name: channelName || undefined,        page_location: pageUrl,        page_path: location.pathname,      });      touchMonthlyActive('twitch_visit', {        visit_reason: reason,        channel_name: channelName || undefined,      });      maybeStartStreamSession(channelName, reason);    } catch (error) {      console.warn('Failed to track visit', error);    }  }  function maybeStartStreamSession(channelName, reason) {    // Switch / leave channel → end the current session before starting a new one    if (activeStreamSession && activeStreamSession.channelName !== channelName) {      endStreamSession('navigate');    }    if (streamSessionStartTimer) {      clearTimeout(streamSessionStartTimer);      streamSessionStartTimer = null;    }    if (!channelName) return;    if (activeStreamSession && activeStreamSession.channelName === channelName) return;    streamSessionStartTimer = setTimeout(() => {      streamSessionStartTimer = null;      // Re-check the channel hasn't changed during the wait.      if (getChannelName() !== channelName) return;      const gameTitle = getGameTitle();      activeStreamSession = {        sessionId: generateLocalId(),        startedAt: Date.now(),        channelName,        gameTitle,      };      sendAnalyticsEvent('stream_session_start', {        stream_session_id: activeStreamSession.sessionId,        channel_name: channelName,        game_title: gameTitle || undefined,        visit_reason: reason || 'load',        page_location: getSafePageLocation(),        page_path: location.pathname,      });    }, STREAM_SESSION_MIN_MS);  }  function endStreamSession(endReason) {    if (!activeStreamSession) return;    const { sessionId, startedAt, channelName, gameTitle } = activeStreamSession;    activeStreamSession = null;    const durationMs = Date.now() - startedAt;    // Re-read game_title at session end — streamer may have switched games.    // Fall back to the value captured at start so we always have something.    const finalGameTitle = getGameTitle() || gameTitle || null;    sendAnalyticsEvent('stream_session_end', {      stream_session_id: sessionId,      channel_name: channelName,      game_title: finalGameTitle || undefined,      duration_ms: durationMs,      duration_sec: Math.round(durationMs / 1000),      end_reason: endReason || 'unknown',    });  }

Event object builder and direct POST path

common/analytics.js
export async function buildOwnEvent(eventName, params, state) {  const userId = (state && (state.userId || state.clientId)) || '';  const sessionId = await getSessionId();  return {    event_id: generateId(),    name: eventName,    ts: Date.now(),    user_id: userId,    session_id: sessionId,    client_id: (state && state.ggselClientId) || null,    properties: pickAllowedProperties(params),    extension_version: getManifestVersion() || null,  };}// When a batch buffer is configured (by background.js), events are queued// instead of sent immediately. Direct send remains the fallback path for// non-service-worker contexts.let ownBackendBuffer = null;export function setOwnBackendBuffer(buffer) {  ownBackendBuffer = buffer && typeof buffer.push === 'function' ? buffer : null;}// Spec section 6.3: flush queue immediately when popup opens (user is// actively engaging — minimize buffering latency for these touchpoints).const FLUSH_AFTER_EVENTS = new Set([  'popup_open',  'extension_install',  'extension_update',  'ggsel_button_click',]);async function sendToOwnBackend(eventName, params, state) {  if (!OWN_ANALYTICS_ENABLED) return false;  if (!OWN_BACKEND_KNOWN_EVENTS.has(eventName)) return false;  if (!state || (!state.userId && !state.clientId)) return false;  const event = await buildOwnEvent(eventName, params, state);  if (!event.user_id) return false;  if (ownBackendBuffer) {    try {      await ownBackendBuffer.push(event);      if (FLUSH_AFTER_EVENTS.has(eventName)) {        // fire-and-forget — flush errors stay inside the buffer        ownBackendBuffer.flush().catch(() => {});      }      return true;    } catch (error) {      console.warn('Own analytics buffer push failed; falling back to direct send', error);    }  }  const controller = new AbortController();  const timer = setTimeout(() => controller.abort(), OWN_ANALYTICS_TIMEOUT_MS);  try {    const response = await fetch(buildEventsEndpoint(), {      method: 'POST',      headers: { 'content-type': 'application/json' },      body: JSON.stringify({ events: [event] }),      credentials: 'omit',      signal: controller.signal,    });    return response.ok;  } catch (error) {    console.warn('Own analytics send failed', error);    return false;  } finally {    clearTimeout(timer);  }}

Service-worker batch POST path

background.js
async function sendOwnAnalyticsBatch(events) {  if (!Array.isArray(events) || events.length === 0) return;  const controller = new AbortController();  const timer = setTimeout(() => controller.abort(), OWN_ANALYTICS_TIMEOUT_MS);  try {    const response = await fetch(buildEventsEndpoint(), {      method: 'POST',      headers: { 'content-type': 'application/json' },      body: JSON.stringify({ events }),      credentials: 'omit',      signal: controller.signal,    });    if (!response.ok) {      throw new Error(`analytics batch HTTP ${response.status}`);    }  } finally {    clearTimeout(timer);  }}

Network destinations involved in this claim

    • twitch.ggsel.net

    Receives the extension-owned JSON event batches for Twitch visits, popup opens, activity markers, and stream sessions.

Where it sends data

Destinations our analysis observed ggsel Ханти: 1080p на Twitch, игры дешевле и Steam в рублях 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.

  • mc.yandex.ru

    ggsel Ханти: 1080p на Twitch, игры дешевле и Steam в рублях sends data to mc.yandex.ru. One other extension we have analyzed sends data here.

  • twitch.ggsel.net

    ggsel Ханти: 1080p на Twitch, игры дешевле и Steam в рублях sends data to twitch.ggsel.net. One other extension we have analyzed sends data here.

Updated 21 September 2026djhlginleaibmpefikimccehaonbgjdp