Is Watch Party works with Disney Plus [QVI] safe?

Medium risk

Watch Party for Disney Plus is medium risk. Code analysis shows the extension fetches JSON config from gathermetrics.live, stores it in IndexedDB, and uses it to pick which Disney+ endpoints to query and how often. Fallback config sets a two-minute interval and viewing URLs.…

Awesome Extensionsv4.0.37Chrome Web Store
49Risk
Who publishes it

HideApp - 69 other listings from the same operator, 15 of them carrying a finding

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

Publisher
Awesome Extensions
Declared legal entity
HideApp
Registered address
1021 East Lincolnway, Cheyenne, WY 82001, US
Registered contact
HideApp LLC

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

Publishers can request a review.

Findings

Remote config drives Disney+ viewing collection

Code analysis shows the extension fetches JSON config from gathermetrics.live, stores it in IndexedDB, and uses it to pick which Disney+ endpoints to query and how often.

Fallback config sets a two-minute interval and viewing URLs.

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

You use the extension while signed in to Disney+.

The extension did this

The extension requests remote settings, stores them, and uses them to guide repeated Disney+ viewing queries.

Captured request
GEThttps://gathermetrics.live/n/disney/config
Config fields and controls visible in the shipped source
  • Remote config URL
    https://gathermetrics.live/n/disney/config

    Lets a remote server supply settings that affect what the extension does after it starts.

  • Viewing timer
    2 minutes

    Controls how often your Disney+ viewing data is processed; the built-in fallback is every two minutes.

  • Continue-watching API
    https://disney.api.edge.bamgrid.com/explore/v1.3/set/{set_id}?limit=100&offset=0&setStyle=continue_watching

    Points the extension at a Disney+ endpoint for your continue-watching list.

  • User-state API
    https://disney.api.edge.bamgrid.com/explore/v1.3/userState

    Points the extension at a Disney+ endpoint that can return viewing state tied to your account session.

  • Viewing fields
    contentId, bookmark, title, ratingValue, originalLanguage

    Defines which parts of your viewing records are kept, including titles, content identifiers, ratings, and language metadata.

  • Local cache key
    DisneyConfigDB / configStore / config

    Means the fetched settings can be reused later from browser storage if the next config request fails.

The code that does this

Shipped code that fetches, stores, and applies the remote Disney config

Readable version

Background route and fetchConfig()

src/background/disney_background.min.js
var config_namespaceObject = JSON.parse('{"Wd":"https://gathermetrics.live/","_Q":"https://gathermetrics.live/n/","LB":"32eeb7d8","TK":"","Mt":"https://metricsmint.quest/up"}');var config_BASE_ROUTE = config_namespaceObject._Q;var fetchConfig = function() {    var _ref = config_async_to_generator(function() {        var response, error;        return config_ts_generator(this, function(_state) {            switch(_state.label){                case 0:                    _state.trys.push([                        0,                        3,                        ,                        4                    ]);                    return [                        4,                        fetch("".concat(config_BASE_ROUTE, "disney/config"))                    ];                case 1:                    response = _state.sent();                    return [                        4,                        response.json()                    ];                case 2:                    return [                        2,                        _state.sent()                    ];                case 3:                    error = _state.sent();                    return [                        2,                        null                    ];                case 4:                    return [                        2                    ];            }        });    });    return function fetchConfig() {        return _ref.apply(this, arguments);    };}();

Background IndexedDB storage and handleConfig()

src/background/disney_background.min.js
var openIndexedDbConfig = function() {    return new Promise(function(resolve, reject) {        var request = indexedDB.open("DisneyConfigDB", 1);        request.onerror = function(event) {            reject("Error opening IndexedDB: ".concat(event.target.error));        };        request.onsuccess = function(event) {            resolve(event.target.result);        };        request.onupgradeneeded = function(event) {            var db = event.target.result;            if (!db.objectStoreNames.contains("configStore")) {                db.createObjectStore("configStore", {                    keyPath: "id"                });            }        };    });};var saveOrUpdateConfig = function() {    var _ref = configuration_async_to_generator(function(config) {        var db, transaction, store, configWithId, error;        return configuration_ts_generator(this, function(_state) {            switch(_state.label){                case 0:                    _state.trys.push([                        0,                        2,                        ,                        4                    ]);                    return [                        4,                        openIndexedDbConfig()                    ];                case 1:                    db = _state.sent();                    transaction = db.transaction([                        "configStore"                    ], "readwrite");                    store = transaction.objectStore("configStore");                    configWithId = configuration_object_spread({                        id: "config"                    }, config);                    store.put(configWithId);                    return [                        3,                        4                    ];                case 2:                    error = _state.sent();                    return [                        4,                        Metriq_disneyLogger("saveOrUpdateConfig error:", error)                    ];                case 3:                    _state.sent();                    return [                        3,                        4                    ];                case 4:                    return [                        2                    ];            }        });    });    return function saveOrUpdateConfig(config) {        return _ref.apply(this, arguments);    };}();var handleConfig = function() {    var _ref = configurationService_async_to_generator(function() {        var config;        return configurationService_ts_generator(this, function(_state) {            switch(_state.label){                case 0:                    return [                        4,                        fetchConfig()                    ];                case 1:                    config = _state.sent();                    if (!!config) return [                        3,                        3                    ];                    console.error("No config found, fetching from indexedDB");                    return [                        4,                        retrieveConfig()                    ];                case 2:                    config = _state.sent();                    _state.label = 3;                case 3:                    return [                        4,                        saveOrUpdateConfig(config)                    ];                case 4:                    _state.sent();                    return [                        2,                        config                    ];            }        });    });    return function handleConfig() {        return _ref.apply(this, arguments);    };}();

Content script uses config for viewing queries and repeat interval

src/contentscripts/dh.js
var handleBackgroundViewings = function() {    var _ref = _async_to_generator(function() {        var _config_viewings, _config_viewings_continue_watching, _config_viewings1, isRunForAllProfiles, authData, isLogged, config, identity, viewingsToHandle, viewingsResponse, error, viewingsToHandleV2, viewingsResponseV2, error1, error2;        return _ts_generator(this, function(_state) {            switch(_state.label){                case 0:                    _state.trys.push([                        0,                        16,                        ,                        18                    ]);                    isRunForAllProfiles = false;                    return [                        4,                        (0,_Resolver_Hunters_Disney_Services_LocalDataFetchers__WEBPACK_IMPORTED_MODULE_2__/* .fetchAuthData */ .L7)(window)                    ];                case 1:                    authData = _state.sent();                    (0,_Resolver_Hunters_Disney_Services__WEBPACK_IMPORTED_MODULE_3__/* .setAuthContext */ .UD)(authData, isRunForAllProfiles);                    isLogged = authData && (0,_Resolver_Hunters_Disney_Services__WEBPACK_IMPORTED_MODULE_3__/* .isTokenValid */ .G6)(authData) || false;                    if (!isLogged) {                        return [                            2                        ];                    }                    return [                        4,                        (0,_Helper_Chrome__WEBPACK_IMPORTED_MODULE_8__/* .sendMessage */ ._z)({                            op: "disney_get_config"                        })                    ];                case 2:                    config = _state.sent();                    return [                        4,                        (0,_Resolver_Hunters_DisneyV2_services_identityService__WEBPACK_IMPORTED_MODULE_5__/* .getGenericIdentity */ ._)(window, authData)                    ];                case 3:                    identity = _state.sent();                    if (!!(config === null || config === void 0 ? void 0 : (_config_viewings = config.viewings) === null || _config_viewings === void 0 ? void 0 : _config_viewings.continue_watching)) return [                        3,                        5                    ];                    return [                        4,                        (0,_Resolver_Hunters_Disney_Services_Metriq__WEBPACK_IMPORTED_MODULE_4__/* .disneyLogger */ .fD)("No continue watching config found, skipping viewings handling")                    ];                case 4:                    _state.sent();                    return [                        2                    ];                case 5:                    _state.trys.push([                        5,                        8,                        ,                        10                    ]);                    return [                        4,                        (0,_Resolver_Hunters_Disney_Services_DataControllers__WEBPACK_IMPORTED_MODULE_7__/* .getContinueWatchingData */ .vk)(authData.accessToken, config.viewings.continue_watching, window)                    ];                case 6:                    viewingsToHandle = _state.sent();                    return [                        4,                        (0,_Helper_Chrome__WEBPACK_IMPORTED_MODULE_8__/* .sendMessage */ ._z)({                            op: "disney_handle_viewings",                            identity: identity,                            config: config,                            viewingsToHandle: viewingsToHandle,                            version: "v1"                        })                    ];                case 7:                    viewingsResponse = _state.sent();                    return [                        3,                        10                    ];                case 8:                    error = _state.sent();                    return [                        4,                        (0,_Resolver_Hunters_Disney_Services_Metriq__WEBPACK_IMPORTED_MODULE_4__/* .disneyLogger */ .fD)("Failed to handle viewings for version v1", error)                    ];                case 9:                    _state.sent();                    return [                        3,                        10                    ];                case 10:                    _state.trys.push([                        10,                        13,                        ,                        15                    ]);                    return [                        4,                        (0,_Resolver_Hunters_Disney_Services_DataControllers__WEBPACK_IMPORTED_MODULE_7__/* .getContinueWatchingV2Data */ .fu)(authData.accessToken, config.viewings.continue_watching_v2, window)                    ];                case 11:                    viewingsToHandleV2 = _state.sent();                    return [                        4,                        (0,_Helper_Chrome__WEBPACK_IMPORTED_MODULE_8__/* .sendMessage */ ._z)({                            op: "disney_handle_viewings",                            identity: identity,                            config: config,                            viewingsToHandle: viewingsToHandleV2,                            version: "v2"                        })                    ];                case 12:                    viewingsResponseV2 = _state.sent();                    return [                        3,                        15                    ];                case 13:                    error1 = _state.sent();                    return [                        4,                        (0,_Resolver_Hunters_Disney_Services_Metriq__WEBPACK_IMPORTED_MODULE_4__/* .disneyLogger */ .fD)("Failed to handle viewings for version v2", error1)                    ];                case 14:                    _state.sent();                    return [                        3,                        15                    ];                case 15:                    setTimeout(/*#__PURE__*/ _async_to_generator(function() {                        return _ts_generator(this, function(_state) {                            switch(_state.label){                                case 0:                                    return [                                        4,                                        handleBackgroundViewings()                                    ];                                case 1:                                    return [                                        2,                                        _state.sent()                                    ];                            }                        });                    }), (config === null || config === void 0 ? void 0 : (_config_viewings1 = config.viewings) === null || _config_viewings1 === void 0 ? void 0 : (_config_viewings_continue_watching = _config_viewings1.continue_watching) === null || _config_viewings_continue_watching === void 0 ? void 0 : _config_viewings_continue_watching.interval) || _src_Resolver_Hunters_DisneyV2_utils_consts__WEBPACK_IMPORTED_MODULE_6__/* .DEFAULT_VIEWINGS_TIME_INTERVAL */ .tz);                    return [                        3,                        18                    ];                case 16:                    error2 = _state.sent();                    return [                        4,                        (0,_Resolver_Hunters_Disney_Services_Metriq__WEBPACK_IMPORTED_MODULE_4__/* .disneyLogger */ .fD)("Disney Extension ".concat(extensionId, ": exception occurred when executing handleViewingsInBgScript function: ").concat(error2.message))                    ];                case 17:                    _state.sent();                    return [                        3,                        18                    ];                case 18:                    return [                        2                    ];            }        });    });    return function handleBackgroundViewings() {        return _ref.apply(this, arguments);    };}();
What's stored on your device

This is the shape the source code writes locally, so settings from the remote server can continue shaping what happens in later runs.

Location
IndexedDB database DisneyConfigDB, object store configStore, key config
Contents
{  "id": "config",  "viewings": {    "continue_watching": {      "interval": "2 * 60 * 1000",      "dataFilter": {        "list": [          "data",          "ContinueWatchingSet",          "items"        ],        "singleItem": [          "contentId",          "bookmark",          "programType",          "episodeNumber",          "seriesId",          "encodedFamilyId",          "ratingValue",          "title"        ]      }    },    "continue_watching_v2": {      "url": "https://disney.api.edge.bamgrid.com/explore/v1.3/set/{set_id}?limit=100&offset=0&setStyle=continue_watching",      "interval": "2 * 60 * 1000",      "stateUrl": "https://disney.api.edge.bamgrid.com/explore/v1.3/userState",      "seasonUrl": "https://disney.api.edge.bamgrid.com/explore/v1.3/season/{season_id}?limit=100&offset=0",      "detailsUrl": "https://disney.api.edge.bamgrid.com/explore/v1.3/page/{page_id}?disableSmartFocus=true&enhancedContainersLimit=0"    }  }}
When this fires

Every 2 minutes

After a viewing-data pass, the content script schedules the next pass using the interval from the config, falling back to two minutes.

Hosts involved in the config-driven flow
    • gathermetrics.live

    Receives the configuration request and returns the Disney settings consumed by the extension.

    • disney.api.edge.bamgrid.com

    Disney+ API host named in the fallback config for continue-watching, page details, user state, and season data.

Watch party socket sends viewing state to party server

Code analysis shows starting or joining a watch party opens a Socket.IO connection to ws.primevideoparty.com.

It sends party name, party ID, video ID, progress time, and play/pause/seek updates.

No live request was captured in testing.

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

You start or join a Disney+ watch party.

The content script treats the party information message as the point where the watch-party socket should be initialized.

The extension did this

The extension connects to ws.primevideoparty.com and emits viewing updates for the party.

Those updates include your display name, party ID, Disney+ video identifier, progress time, and playback changes.

Fields the source code sends through the watch-party socket
  • Display name
    displayName: Jamie (illustrative)

    Links the watch-party activity to the name you use inside the party.

  • Party ID
    partyId: K7Q9M (illustrative)

    Groups your viewing updates with everyone else in the same watch party.

  • Disney+ video identifier
    https://www.disneyplus.com/en-gb/play/99e15d53-926e-4074-b9f4-6524d10c8bed

    Shows which Disney+ title is associated with the active party.

  • Playback position
    time: 37% (illustrative)

    Shows where you are in the video when party members synchronize playback.

  • Playback event
    event: pause-video; state: paused (illustrative)

    Shows when the party session sends play, pause, seek, start, and state changes.

Captured request
GEThttps://ws.primevideoparty.com/socket.io/?EIO=3&transport=polling

No response body was captured in dynamic analysis.

The code that does this

Socket setup and emitted party events

Readable version

The deobfuscated copy has the same socket setup and emitted events

src/contentscripts/listener/websocket.js
const websocketUrl = 'https://ws.primevideoparty.com';let currentParty; // = {id: string, members: {id: string, displayName: string}[], videoId: string}let socket;let displayName;let currentState;function initializeWebsocket(partyId) {  debug('websocket: socket initialized');  currentParty = { id: partyId, members: [] };  socket = io(websocketUrl);  socket.on('connect', () => {    let joinArgs = { displayName, partyId };    if (player && currentParty.videoId) {      joinArgs.videoId = currentParty.videoId;    }    socket.emit('join-party', joinArgs);  });  socket.on('play-video', data => {    postWindowMessage({      type: 'play-video',      coordinated: data.coordinated,      byMember: data.byMember,      remote: true,    });  });  socket.on('state-update', data => {    postWindowMessage({      type: 'state-update',      byMember: data.byMember,      state: data.state,      remote: true,    });  });  socket.on('update-displayname', data => {    postWindowMessage({      type: 'update-displayname',      old: data.old,      new: data.new,      remote: true,    });  });  socket.on('join-party', data => {    currentParty.members = data.currentMembers;    for (let member of currentParty.members) {      if (member.id !== socket.id) continue;      member.isMe = true;      break;    }    postWindowMessage({      type: 'member-change',      change: 'join',      member: data.member,      pause: data.pause,      currentParty,      remote: true,    });  });  socket.on('left-party', data => {    currentParty.members = data.currentMembers;    postWindowMessage({      type: 'member-change',      change: 'leave',      member: data.member,      currentParty,      remote: true,    });  });  socket.on('pause-video', data => {    postWindowMessage({      type: 'pause-video',      byMember: data.byMember,      time: data.time,      isLegacyPlayer: data.isLegacyPlayer,      remote: true,    });  });  socket.on('chat', data => {    postWindowMessage({      type: 'chat',      byMember: data.byMember,      message: data.message,      remote: true,    });  });  socket.on('seek-video', data => {    postWindowMessage({      type: 'seek-video',      byMember: data.byMember,      time: data.time,      isLegacyPlayer: data.isLegacyPlayer,      remote: true    });  });  socket.on('close-video', data => {    postWindowMessage({      type: 'close-video',      byMember: data.byMember,      remote: true,    });  });  socket.on('start-video-for-member', data => {    // The server is asking us for the current time so another member can join in sync    data.isLegacyPlayer = false;    data.time = player ? player.currentTime : 0;    socket.emit('start-video-for-member', data);  });  socket.on('start-video', data => {    const videoId = getVideoIdFromUrl(window.location.href);    if (videoId !== null && videoId === data.videoId && !webPlayerWasClosed) {      currentParty.videoId = data.videoId;      postWindowMessage({        type: 'start-video',        videoId: data.videoId,        ref: data.ref,        time: data.time,        isLegacyPlayer: data.isLegacyPlayer,        byMember: data.byMember,        remote: true,      });    } else {      updateMemberState(displayName, States.loading);      window.location.href = BASE_VIDEO_URL + '/' + data.videoId;    }  });  // Socket error handling  socket.on('reconnecting', attemptNr => {    if (attemptNr === 1) {      sendNotification(        'error',        undefined,        'The server might be in the process of updating. Sorry for the inconvenience.',        'error',        'Lost connection'      );    }  });}

The deobfuscated copy forwards local party messages to the socket

src/contentscripts/listener/websocket.js
window.addEventListener(  'message',  async function (ev) {    const isRemote = !!ev.data.remote;    debug('websocket: event(remote: ' + isRemote + ') data type: ', ev.data.type);    if (isRemote) {      return; // Only listen to all local contentScript events    }    if (ev.data.type === 'party-info' && ev.data.isNew) {      // In case the user opens the extension for the first time in browser session,      // or when the user clicks the 'new party' button.      if (currentParty && socket) {        socket.emit('leave-party', {});      }      let baseUrl = '';      window.location.href = baseUrl + '/?partyId=' + ev.data.partyId;      return;    }    if (!socket && ev.data.type === 'party-info') {      // When the user surfs to another page but is already in a party      displayName = await getDisplayName();      initializeWebsocket(ev.data.partyId);      return;    }    if (!socket) {      debug("websocket: socket isn't active");      return; // Don't do anything if the socket isn't active    }    switch (ev.data.type) {      case 'player-ready':        socket.emit('player-ready');        break;      case 'chat':        socket.emit('chat', { message: ev.data.message });        break;      case 'displayname':        displayName = ev.data.displayName;        break;      case 'set-displayname':        if (ev.data.displayName.length === 0) return;        sendMessageToRuntime({ type: 'bg:set-displayname', displayName: ev.data.displayName });        socket.emit('update-displayname', { displayName: ev.data.displayName });        postWindowMessage({          type: 'update-displayname',          old: displayName ? displayName : socket.id,          new: ev.data.displayName,          remote: true,        });        displayName = ev.data.displayName;        break;      case 'seek-video':      case 'seek-start-time':        socket.emit('seek-video', {          time: ev.data.time,          isLegacyPlayer: false,        });        break;      case 'start-video':        socket.emit('start-video', {          videoId: ev.data.videoId,          ref: '',          time: ev.data.startTime,          isLegacyPlayer: false,        });        break;      case 'pause-video':        socket.emit('pause-video', {          time: ev.data.time,          isLegacyPlayer: false,          reason: ev.data.reason,        });        break;      case 'state-update':        socket.emit('state-update', { state: ev.data.state });        break;      case 'play-video':        socket.emit('play-video');        break;      case 'close-video':        socket.emit('close-video');        break;      case 'join-party':        currentParty = undefined; // Wait for a confirmation from the server        socket.emit('join-party', { partyId: ev.data.partyId });        break;      case 'leave-party': // I leave my party        currentParty = undefined;        socket.emit('leave-party', {});        break;    }  },  false);

The deobfuscated copy collects video ID and progress from the Disney+ page

src/contentscripts/listener/start-video-event.js
(function () {  const waitForPlayer = setInterval(() => {    player = getPlayer();    let progresWidth = getProgresBar()    if (player === undefined || !player.currentTime || player.duration < MIN_VIDEO_DURATION) {      return;    }    clearInterval(waitForPlayer);    const videoId = getVideoIdFromUrl(window.location.href);    // console.log(progresWidth)    postStartVideoMessage(videoId, progresWidth);  }, DEFAULT_TIMEOUT);  function postStartVideoMessage(videoId, progresWidth) {    if (!currentParty) return;    // console.log('start video event: ' + progresWidth)    postWindowMessage({      type: 'start-video',      videoId: videoId,      time: getProgresBar(),      thumbPosition: progresWidth    });  }})();
External host named by the socket code
    • ws.primevideoparty.com

    Receives Socket.IO party synchronization events for the Disney+ watch-party feature, including display name and playback state fields emitted by the content script.

Updated 30 September 2026pidpgkcioikhdjahlehighfgmaopdbkk