Is Netflix Dual subtitle for learning languages safe?

High risk

Netflix Dual subtitle for learning languages loads Netflix page code from Google Cloud Storage when a remote manifest enables it.

The extension injects code into Netflix pages and checks a Google Cloud Storage manifest that can make it load a remote replacement script for that page logic. It also stores the Google sign-in JWT in Netflix localStorage with a bundled AES passphrase and later sends the token to its license API.

72Risk
Who publishes it

niko - 2 other listings from the same operator, 1 of them carrying a finding

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

Same store account

2 other listings published from this account, 26k+ users between them. 1 of them carries a finding.

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

Publishers can request a review.

Findings

Remote Script Can Replace Netflix Page Integration

When Netflix loads, the extension injects its bundled page integration and requests a GCS manifest.

If it returns an enabled flag, the page loads a replacement insert script from GCS with the same context as the integration.

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

You open a Netflix page while the extension is active.

The extension did this

The extension checks a hosted update manifest and can load a hosted replacement for its Netflix page script.

The hosted script is added to the page only when the manifest response contains an enabled update flag.

Captured request
GEThttps://storage.googleapis.com/niko-extension/netflix-dual-subtitle-for-learning-languages/mv3/manifest.json
Remote update controls and page scope
  • Update manifest
    https://storage.googleapis.com/niko-extension/netflix-dual-subtitle-for-learning-languages/mv3/manifest.json

    This remote file decides whether the page keeps the packaged script or loads hosted code instead.

  • Update flag
    {"update":true}

    When this flag is enabled, the page loads code from the hosted script URL.

  • Hosted script
    https://storage.googleapis.com/niko-extension/netflix-dual-subtitle-for-learning-languages/mv3/insert.min.js

    This is the replacement script URL that the page loads when the remote flag is enabled.

  • Injected script ID
    dualSubtitleForLearningLanguages-secondary-subtitle-wrapperGoogleCloudInsertMinJs

    This page element marks the hosted script that the extension adds to Netflix.

  • Netflix page URL
    https://www.netflix.com/watch/81234567

    The hosted page script runs in the Netflix page you are using, including the page address and page content available there.

The code that does this

The packaged content script injects the Netflix page integration

Readable version

Readable equivalent of the content script injection path

content.min.js
const logger = require(7434);const appName = require(8489).APP_NAME;function injectHeadScript() {  if (!document.querySelector("head")) return false;  const scriptId = `${appName}InsertJSToHead`;  if (document.getElementById(scriptId)) return false;  const file = "insert_to_head.min.js";  const script = document.createElement("script");  script.onload = function () {    logger.log(`Injected: ${file}  Epoch time: ${Date.now()}`);  };  script.async = false;  script.setAttribute("id", scriptId);  script.setAttribute("type", "text/javascript");  script.setAttribute("src", chrome.runtime.getURL(file));  document.head.appendChild(script);  return true;}const observer = new MutationObserver(function (mutations) {  mutations.forEach(function (mutation) {    mutation.addedNodes.forEach(function (node) {      if (node.nodeName.toLowerCase() === "head" || node.nodeName.toLowerCase() === "body") {        if (injectHeadScript()) observer.disconnect();      }    });  });});observer.observe(document, { childList: true, subtree: true });if (injectHeadScript()) observer.disconnect();window.addEventListener("DOMContentLoaded", function () {  ["insert.min.js"].forEach(function (file) {    const script = document.createElement("script");    script.onload = function () {      logger.log(`Injected: ${file}  Epoch time: ${Date.now()}`);    };    script.async = false;    script.setAttribute("type", "text/javascript");    script.setAttribute("src", chrome.runtime.getURL(file));    document.head.appendChild(script);  });});

Readable manifest scope

manifest.json
"host_permissions": [  "https://www.netflix.com/*"],"content_scripts": [  {    "matches": [      "https://www.netflix.com/*"    ],    "js": [      "content.min.js"    ],    "run_at": "document_start"  }],"web_accessible_resources": [  {    "resources": [      "insert.min.js",      "insert_to_head.min.js"    ],    "matches": [      "https://www.netflix.com/*"    ]  }]
The code that does this

The injected script gates hosted code on a remote manifest flag

Readable version

Readable equivalent of the remote update branch

insert.min.js
const googleCloudScriptId = `${secondarySubtitleWrapperId}GoogleCloudInsertMinJs`;new Promise(function (resolve) {  if (document.getElementById(googleCloudScriptId)) {    logger.log("Emergency update already exists");    resolve(false);    return;  }  new Promise(function (resolveManifest, rejectManifest) {    const xhr = new XMLHttpRequest();    xhr.responseType = "json";    xhr.open(      "GET",      "https://storage.googleapis.com/niko-extension/netflix-dual-subtitle-for-learning-languages/mv3/manifest.json",      true    );    xhr.timeout = 15000;    xhr.onload = function () {      if (xhr.status === 200) {        resolveManifest(xhr.response);      } else {        logger.error(`xhr manifest error: ${xhr.statusText} status: ${xhr.status}`);        rejectManifest();      }    };    xhr.onerror = function () {      logger.error(`xhr manifest error: ${xhr.statusText}`);      rejectManifest();    };    xhr.ontimeout = function () {      logger.error(`xhr manifest timeout: ${xhr.statusText}`);      rejectManifest();    };    xhr.send(null);  })    .then(function (manifest) {      if (manifest.update) {        logger.log("Emergency update is enabled");        resolve(true);      } else {        logger.log("Emergency update is not enabled");        resolve(false);      }    })    .catch(function () {      resolve(false);    });}).then(function (enabled) {  if (enabled) {    new Promise(function (resolve, reject) {      const url = "https://storage.googleapis.com/niko-extension/netflix-dual-subtitle-for-learning-languages/mv3/insert.min.js";      const script = document.createElement("script");      script.id = googleCloudScriptId;      script.onload = function () {        logger.log(`Injected: ${url}  Epoch time: ${Date.now()}`);        resolve();      };      script.onerror = function () {        logger.error("Emergency update failed to load");        reject();      };      script.setAttribute("type", "text/javascript");      script.setAttribute("src", url);      (document.head || document.documentElement).appendChild(script);    }).catch(function () {      startPackagedIntegration();    });  } else {    startPackagedIntegration();  }});
Remote host used by the update path
    • storage.googleapis.com

    Google Cloud Storage host for the update manifest and the hosted replacement insert script.

Where it sends data

Destinations our analysis observed NetflixDualSubtitle 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.

  • storage.googleapis.com

    NetflixDualSubtitle sends data to storage.googleapis.com. 7 other extensions we have analysed send data here.

  • niko-pay.appspot.com

    NetflixDualSubtitle sends data to niko-pay.appspot.com. No other extension we have analysed sends data here.

Updated 30 September 2026oejbkdefifpkcldpkdfkhigapiifmjcl