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.

UdemyDualSubtitlesv4.1.0Chrome Web Store
45Risk

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

Publishers can request a review.

Findings

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Subtitle translation sends Udemy tab details to backend

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.

01EvidenceCAUSE EFFECT
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.

02EvidenceFIELD TABLE
Fields included in the translation POST
FieldValueWhy it matters
Installation identifier
4f8a2b7c-91d3-4c6e-a51d-6b0f9a8e1234Lets 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/12345678Shows the exact course or lecture page where you requested translation.
Page origin
https://www.udemy.comShows the site context for the translation request.
Page title
Python for Data Science - Lecture 12: Pandas BasicsReveals 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=esShows the source and target languages selected for the translation.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://api.udemydualsubtitles.com/translate
Headers
Content-Typeapplication/json
04EvidenceCODE COMPARE
The code that does this

The translation path carries tab details into the backend request

What it actually does
Content script sends the translate commandcontent.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.cidbackground.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 JSONbackground.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;
  });
05EvidenceTHIRD PARTY LIST
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.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Runtime messages attach Udemy tab details to Amplitude

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.

01EvidenceCAUSE EFFECT
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.

02EvidenceFIELD TABLE
Analytics properties populated by the service worker
FieldValueWhy it matters
Current tab URL
https://www.udemy.com/course/python-for-data-science/learn/lecture/12345678Shows which Udemy page was open when the extension processed the message.
Current tab title
Python for Data Science - Lecture 12: Pandas BasicsReveals 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=esIncludes the command and payload associated with the feature you used.
Changed storage values
key=lastSubtitleLang, oldValue=en, newValue=esRecords extension settings or state before and after a storage update.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://api2.amplitude.com/2/httpapi
04EvidenceCODE COMPARE
The code that does this

Runtime messages and storage changes flow into Amplitude events

What it actually does
Amplitude HTTP API endpointbackground.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 trackbackground.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 fieldsbackground.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 valuesbackground.js
kr("Bg Storage Change", {
  extname: f.ext.name,
  namespace: t,
  key: u,
  oldValue: a,
  newValue: c,
});
sendBeacon transport serializes the event payloadbackground.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 });
05EvidenceTHIRD PARTY LIST
Analytics destination
  • api2.amplitude.com

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

Data recipients

api.udemydualsubtitles.comapi2.amplitude.com
Updated 17 September 2026mpedooclpiggpfamidjeemfjoeakebce