Is Udemy Dual Subtitles - Subtitle Translator safe?

Medium risk

Udemy Dual Subtitles sends tab URL, tab title, and a persistent user UUID to its own backend and to Amplitude on every subtitle translation request.

Each time the extension translates subtitles, it POSTs the current tab URL, tab title, source and target languages, and a persistent device UUID to api.udemydualsubtitles.com. Separately, the background service worker forwards tab URL and tab title as properties of every runtime message event to Amplitude's analytics API, including changes to extension storage. The UUID is generated once and stored in chrome.storage.local, allowing the developer to correlate requests across sessions.

UdemyDualSubtitlesv5.2.0Chrome Web Store
49Risk

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

Publishers can request a review.

Findings

Subtitle translation sends Udemy tab details to backend

MediumunwantedCWE-359Dynamic sandbox

The source code shows a translation request sends the Udemy tab URL, tab title, page origin, subtitle text, and language choices to api.udemydualsubtitles.com, plus an install UUID from local storage (ga.cid), generated on first use.

What actually happens

You did this

You request translated subtitles while viewing a Udemy course page.

The content script packages the selected subtitle text with source and target languages.

The extension did this

The extension posts that subtitle request with the Udemy tab URL, tab title, page origin, and stored installation UUID.

The request goes to api.udemydualsubtitles.com over HTTPS.

Fields included in the translation POST

  • Installation identifier
    4f8a2b7c-91d3-4c6e-a51d-6b0f9a8e1234

    Lets separate translation requests be tied back to the same extension installation.

  • Udemy page URL
    https://www.udemy.com/course/python-for-data-science/learn/lecture/12345678

    Shows the exact course or lecture page where you requested translation.

  • Page origin
    https://www.udemy.com

    Shows the site context for the translation request.

  • Page title
    Python for Data Science - Lecture 12: Pandas Basics

    Reveals the course or lesson title visible in the browser tab.

  • Subtitle text
    In this lecture we will load the CSV file and inspect the first rows.

    Contains the caption text selected for translation.

  • Language choices
    from=en, to=es

    Shows the source and target languages selected for the translation.

Captured request

POSThttps://api.udemydualsubtitles.com/translate
Headers
Content-Type
application/json

The code that does this

The translation path carries tab details into the backend request

Readable version

Content script sends the translate command

content.js
return (  C.debug("sendMessage, cmd: translate, from:", t, "to:", n),  (e.next = 3),  chrome.runtime    .sendMessage({      cmd: "translate",      payload: { from: t, to: n, q: B(r.slice(o, o + u)) },    })    .then(function (e) {      C.debug("sendMessage[translate] rsp:", JSON.stringify(e));      if (!e.errcode) {        _(!1);        _(function (t) {          C.debug("getCues2Processing state:", t);          v(W(r, g, e.rslt));          return t;        });      }    }));

Service worker reads or creates ga.cid

background.js
U = (function () {  var e = u(    a().mark(function e() {      var t, n, r;      return a().wrap(function (e) {        for (;;)          switch ((e.prev = e.next)) {            case 0:              return (                (t = f.storageKeys.gaClientId),                (n = null),                (e.next = 1),                chrome.storage.local.get([t])              );            case 1:              return (                (r = e.sent),                m.debug("storage get:", r),                r[t]                  ? (m.debug("got id from storage: ", r[t]), (n = r[t]))                  : ((n = k()),                    m.debug("generate id from uuidv4: ", n),                    chrome.storage.local.set(p({}, t, n))),                e.abrupt("return", n)              );            case 2:            case "end":              return e.stop();          }      }, e);    }),  );  return function () {    return e.apply(this, arguments);  };})();

Service worker builds and posts the translation JSON

background.js
(P = e.sent),(N = n.origin),(C = null == n || null === (u = n.tab) || void 0 === u ? void 0 : u.url),(j = null == n || null === (s = n.tab) || void 0 === s ? void 0 : s.title),(L = t.payload.from),(U = t.payload.to),(D = t.payload.q),(M = O.stringify.call(O, {  ext: "UdemyDualSubtitles",  cid: P,  origin: N,  tab_url: C,  tab_title: j,  from: L,  to: U,  q: D,})),(e.next = 4),x(k, { method: "POST", mode: "cors", headers: T, body: M })  .then(function (e) {    return e.json();  })  .then(function (e) {    return e.trans_result;  })  .catch(function (e) {    return m.error(e), null;  });

Destination receiving the translation request

    • api.udemydualsubtitles.com

    Extension backend that receives subtitle translation POSTs with cid, origin, tab_url, tab_title, language choices, and q.

Runtime messages attach Udemy tab details to Amplitude

MediumunwantedCWE-359Dynamic sandbox

The source code logs each background runtime message as an Amplitude event, Bg Runtime Message, with properties including the request object plus sender.url/tab.title/tab.url, sent to api2.amplitude.com/2/httpapi.

What actually happens

You did this

You use an extension feature that sends a message to the background service worker.

Translation, license, and usage commands are routed through the runtime-message handler.

The extension did this

The service worker records the message as an Amplitude analytics event with the current tab URL and title.

The same service worker also records extension storage changes with old and new values.

Analytics properties populated by the service worker

  • Current tab URL
    https://www.udemy.com/course/python-for-data-science/learn/lecture/12345678

    Shows which Udemy page was open when the extension processed the message.

  • Current tab title
    Python for Data Science - Lecture 12: Pandas Basics

    Reveals the course or lesson title visible in the browser tab.

  • Sender URL
    https://www.udemy.com/course/python-for-data-science/learn/

    Adds the page context that sent the message to the background service worker.

  • Extension request
    cmd=translate, from=en, to=es

    Includes the command and payload associated with the feature you used.

  • Changed storage values
    key=lastSubtitleLang, oldValue=en, newValue=es

    Records extension settings or state before and after a storage update.

Captured request

POSThttps://api2.amplitude.com/2/httpapi

The code that does this

Runtime messages and storage changes flow into Amplitude events

Readable version

Amplitude HTTP API endpoint

background.js
var ce = "".concat("AMP", "_unsent"),  le = "https://api2.amplitude.com/2/httpapi",  fe = "dclid",  de = "fbclid",  pe = "gbraid",  he = "gclid",  ve = "ko_click_id",  ge = "li_fat_id";

Analytics wrapper calls Amplitude track

background.js
kr = (function () {  var e = u(    a().mark(function e(t, n) {      var r;      return a().wrap(        function (e) {          for (;;)            switch ((e.prev = e.next)) {              case 0:                return ((e.prev = 0), (e.next = 1), Sr(t, n));              case 1:                e.next = 3;                break;              case 2:                ((e.prev = 2), (r = e.catch(0)), m.error("[amp] catch err:", r.message));              case 3:              case "end":                return e.stop();            }        },        e,        null,        [[0, 2]],      );    }),  );  return function (t, n) {    return e.apply(this, arguments);  };})();

Runtime message event includes request and tab fields

background.js
kr("Bg Runtime Message", {  extname: f.ext.name,  status: "begin",  cmd: e.cmd,  req: e,  sender: {    url: null == t ? void 0 : t.url,    tab: {      title: null == t || null === (r = t.tab) || void 0 === r ? void 0 : r.title,      url: null == t || null === (i = t.tab) || void 0 === i ? void 0 : i.url,    },  },});var o = null;return "translate" === e.cmd  ? (Pr(e, t, n), !0)  : "license" === e.cmd    ? (Rr(e, t, n), !0)    : "usage" === e.cmd      ? (Cr(e, t, n), !0)      : (m.error("wrong req.cmd"),        n((o = { errcode: -1, errmsg: "wrong req.cmd" })),        void m.debug("rsp:", o));

Storage changes include old and new values

background.js
kr("Bg Storage Change", {  extname: f.ext.name,  namespace: t,  key: u,  oldValue: a,  newValue: c,});

sendBeacon transport serializes the event payload

background.js
o.navigator.sendBeacon(e, JSON.stringify(t))  ? n.buildResponse({      code: 200,      events_ingested: t.events.length,      payload_size_bytes: u.length,      server_upload_time: Date.now(),    })  : n.buildResponse({ code: 500 });

Analytics destination

    • api2.amplitude.com

    Amplitude HTTP API endpoint that receives extension analytics events and their event properties.

Where it sends data

Destinations our analysis observed Udemy Dual Subtitles - Subtitle Translator 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.

  • api2.amplitude.comwidely used

    Udemy Dual Subtitles - Subtitle Translator sends data to api2.amplitude.com. A widely used service: 42 other extensions we have analyzed send data here.

  • api.udemydualsubtitles.com

    Udemy Dual Subtitles - Subtitle Translator sends data to api.udemydualsubtitles.com. No other extension we have analyzed sends data here.

Updated 30 September 2026mpedooclpiggpfamidjeemfjoeakebce