Is DeepTranslate - Full Page & Bilingual Translator & AI-Powered Translation safe?

Medium risk

DeepTranslate routes all translated page text through its own proxy server and tracks each installation with a persistent UUID sent on every request.

When a user triggers translation, the full text content of the current page is sent to deeptranslate.ai servers as JSON โ€” bypassing any user-configured OpenAI key. Every request to deeptranslate.ai carries a device-unique X-Plugins-Id header generated at install time, along with the user's timezone, browser language, and version. The extension also periodically fetches remote per-site configuration rules from deeptranslate.ai, which can alter how it behaves on any URL, and fires Google Analytics events on translation actions using a second persistent client ID.

deeptranslatev1.7.7Chrome Web Store
49Risk

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

Publishers can request a review.

Findings

AI Translation Sends Page Text to DeepTranslate Proxy

Using OpenAI page translation posts page text to deeptranslate.ai/trans/.../textTranslate.

The wrapper adds a persistent plugin ID, version, timezone, and auth token if logged in.

No captured body existed; details are from source.

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

You use the OpenAI page translation feature on a website.

The content script starts translating the current page after the feature is invoked.

The extension did this

The extension sends page text chunks and page context to DeepTranslate for translation.

The code path sends the request through the extension developer's proxy instead of calling an OpenAI endpoint directly.

Fields built for the DeepTranslate textTranslate request
  • Page text chunks
    Quarterly roadmap: customer onboarding improvements (illustrative)

    The text you asked to translate leaves the page and is sent to the translation proxy.

  • Page URL
    https://intranet.example.com/quarterly-roadmap (illustrative)

    The request can tie translated text to the page where you were reading it.

  • Chunk identifiers
    0-3

    These identifiers let the service match returned translations to the original page fragments.

  • Target language
    Japanese

    This tells the service which language you selected for the translated page.

  • Persistent plugin ID
    f6d01c54-7b0c-4c3d-9eaf-2e13b2a15c89 (illustrative)

    This can link multiple translation requests from the same browser installation.

  • Extension version
    1.7.5

    This adds software-version context to each request.

  • Timezone
    America/New_York

    This adds regional context that can narrow where you are using the browser.

  • Signed-in token
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature (illustrative)

    When you are logged in, the request can include an account token for the translation service.

  • Static request signature
    PC6/JXgymOhlSbt9gEMq03LgdduKB0++YN8iH5CWnyU=

    The request includes a fixed signature value used by the extension's proxy API.

Captured request
POSThttps://deeptranslate.ai/trans/portal/translate/textTranslate

No captured textTranslate request body was present in the evidence rows; the request body structure is shown in the code block.

Headers
Sign
PC6/JXgymOhlSbt9gEMq03LgdduKB0++YN8iH5CWnyU=
X-Timezone
America/New_York (illustrative)
Content-Type
application/json
X-Plugins-Id
f6d01c54-7b0c-4c3d-9eaf-2e13b2a15c89 (illustrative)
X-Client-Version
1.7.5
The code that does this

Source path from page text collection to DeepTranslate proxy request

Readable version

Content script collects page text and forwards it for translation

content-scripts/content.js
    async collectNodesToTranslate() {      const l = document.body.querySelectorAll("iframe");      for (const f of l)        if (f.contentDocument?.body) {          const g = (await io(f.contentDocument.body, this.ctx)).map(h => new Yn(h));          this.piecesToTranslate.push(...g)        } const u = (await io(document.body, this.ctx)).map(f => new Yn(f));      this.piecesToTranslate.push(...u), await this.collectShadowRootNodesToTranslate()    }    async collectShadowRootNodesToTranslate() {      const l = Vi();      for (const r of l) {        ro(r);        for (const u of _s(r)) {          const d = (await io(u, this.ctx)).map(g => new Yn(g));          this.piecesToTranslate.push(...d)        }      }    }    restoreShadowRootInfluence() {      for (const l of Vi()) {        yv(l);        for (const r of _s(l)) ji(r)      }    }    getDocumentTextForLanguageDetection() {      const l = [document.body?.innerText ?? ""];      for (const r of Vi()) {        const u = Array.from(r.children).map(f => f instanceof HTMLElement ? (f.innerText || f.textContent || "").trim() : "").filter(Boolean).join(``);        u && l.push(u)      }      return l.join(``).trim()    }    translatePageTitle() {      const l = document.querySelector("title");      l && !_a(l, !1) || document.title.trim().length < 1 || (this.originalPageTitle = document.title, os(this.currentPageTranslatorService, this.currentTargetLanguage, this.originalPageTitle).then(r => {        r && (document.title = r)      }))    }    async translateDynamically() {      try {        if (this.piecesToTranslate && this.pageIsVisible) {          const l = this.ctx.pageSpecialConfig?.preHeight ?? [0, 0],            r = l.length === 2 ? l : [...l, ...l],            u = g => {              if (!g) return !1;              const h = g.getBoundingClientRect();              return h.top >= 0 && h.top <= window.innerHeight + r[0] * window.innerHeight            },            f = g => {              if (!g) return !1;              const h = g.getBoundingClientRect();              return h.bottom >= -r[1] * window.innerHeight && h.bottom <= window.innerHeight            },            d = [];          this.piecesToTranslate.forEach(g => {            if (!g.isTranslated) {              const h = g.target;              (this.isPDFAndTranslateAllText || this.isEpub || f(h) || u(h)) && (g.isTranslated = !0, d.push(g))            }          }), this.piecesToTranslate = this.piecesToTranslate.filter(g => !g.isTranslated), await this.splitAndTranslatePieces(d), this.firstTranslate !== null && !this.firstTranslate && (window.postMessage({            action: "translated",            done: !0          }, "*"), this.firstTranslate = !0), this.mutationHandler.takeRecords()        }      } catch {}      setTimeout(() => this.translateDynamically(), 200)    }    async splitAndTranslatePieces(l, r = {}) {      const u = this.fooCount,        f = await Ce.getServiceMaxTextLength(this.currentPageTranslatorService),        d = new Map;      let g = [0, 50, 200, 500];      f && (g = g.filter(p => p < f), g.push(f));      const h = p => {        for (let b = 0; b < g.length; b++)          if (p <= g[b + 1]) return g[b];        return g[g.length - 1]      };      for (const p of l) {        const b = p.simplifiedInnerHTML.length;        if (f && b > f) continue;        const x = {            textLength: h(b),            langCode: "auto"          },          O = JSON.stringify(x);        d.has(O) || d.set(O, []), d.get(O).push(p)      }      const v = [];      for (const [p, b] of d) {        const x = JSON.parse(p);        v.push(this.translatePiecesToTranslate(b, u, {          sourceLanguage: x.langCode,          timeout: x.textLength > 500 ? -1 : 15e3,          ...r        }))      }      await Promise.allSettled(v)    }    async translatePiecesToTranslate(l, r, u = {}) {      const f = {        translateUrl: this.ctx.tabUrl,        ...u      };      if (l.length !== 0) try {        const d = await Promise.all(l.map(async h => {            const v = h.parent;            return id(v), await Nv(h.simplifiedInnerHTML)          })),          g = await rs(this.currentPageTranslatorService, d, {            sourceLanguage: "auto",            targetLanguage: this.currentTargetLanguage,            dontSortResults: this.dontSortResults,            ...f          });        if (this.pageLanguageState === "translated" && r === this.fooCount)          for (let h = 0; h < l.length; h++) this.translatePtt(l[h], g[h])      } catch (d) {        if (this.pageLanguageState === "translated" && r === this.fooCount)          for (const g of l) this.handleResultError(g, d)      }    }

AI translation service builds the textTranslate payload

background.js
  class at extends Ge {    constructor(e) {      super(e, tt + "/portal/translate/textTranslate", "POST", function(a) {        function n(o) {          const i = /[\uDC30-\uDC39]/g,            c = /[\u{1F600}-\u{1F64F}|\u{1F300}-\u{1F5FF}|\u{1F680}-\u{1F6FF}|\u{2600}-\u{26FF}|\u{2700}-\u{27BF}]/gu;          return o.replace(i, "").replace(c, "")        }        const s = a.map((o, i) => ({          content: H.escapeHTML(n(o)),          id: i.toString()        }));        return JSON.stringify(s)      }, function(a) {        const n = [],          s = a?.data;        if (!s) throw new M({          source: "server",          message: a.message ?? "Empty response",          code: a.code,          traceId: a.traceId        });        const o = new Map;        for (let i = 0; i < s.texts.length; i++) {          const c = s.texts[i],            h = c.id.split("-"),            f = h[0];          o.has(f) || o.set(f, []), o.get(f).push({            content: c.translation,            id: h[1]          })        }        for (const [i, c] of o) n[i] = {          text: JSON.stringify(c),          detectedLanguage: s.targetLanguage        };        return n      }, function(a, n) {        if (!a) return [];        const s = JSON.parse(a),          o = [];        for (const i of s) typeof i.content == "string" && (o[i.id] = H.unescapeHTML(i.content));        return o      }, function(a, n) {        return ""      }, function(a, n) {        let {          sourceLanguage: s,          targetLanguage: o,          translateUrl: i,          translatorCode: c = "1",          promptBuilderCode: h = 0        } = n;        o === "ja" && (o = "Japanese");        const f = [];        a.map((b, v) => {          const R = JSON.parse(b.originalText);          for (const g of R) g.content.length > 0 && f.push({            id: v + "-" + g.id,            content: g.content          })        });        const w = {          translateUrl: i,          targetLanguage: o,          translatorCode: c,          promptBuilderCode: h,          texts: f        };        return s !== "auto" && (w.sourceLanguage = s), JSON.stringify(w)      }, function() {        return {          "Content-Type": "application/json",          Sign: "PC6/JXgymOhlSbt9gEMq03LgdduKB0++YN8iH5CWnyU="        }      })    }    async translate(e, t) {      if (this.serviceName === "openai") {        const n = await E.checkAndConsumeOpenAIQuota();        if (console.log("๐Ÿš€ ~ AI Service ~ translate ~ quotaCheck:", n), !n.allowed) {          try {            const s = await chrome.tabs.query({              active: !0,              currentWindow: !0            });            s.length > 0 && s[0].id && chrome.tabs.sendMessage(s[0].id, {              action: y.SHOW_LOGIN_POPUP_FOR_QUOTA_EXCEEDED            })          } catch (s) {            console.error("๐Ÿš€ ~ Failed to send quota exceeded message to content script:", s)          }          throw new M({            source: "client",            message: `ๆœช็™ปๅฝ•็”จๆˆทๅ…่ดนOpenAI็ฟป่ฏ‘ๆฌกๆ•ฐๅทฒ็”จๅฎŒ๏ผŒ่ฏท็™ปๅฝ•ไฝฟ็”จๆ›ดๅคš็ฟป่ฏ‘ๆœๅŠกใ€‚ๅ‰ฉไฝ™ๆฌกๆ•ฐ: ${n.remaining}`,            code: 403          })        }        n.message === "quota_consumed" && n.remaining >= 0 && console.log(`๐Ÿš€ ~ OpenAI translation used, remaining: ${n.remaining} free uses`)      }      const a = await this.getTranslatorOptions();      return t = {        ...t,        ...a      }, super.translate(e, t)    }    async getTranslatorOptions() {      const [e, t] = await Promise.all([Ce.getValue(), this.getTranslatorCode()]);      return {        translatorCode: t,        promptBuilderCode: e.selectCode      }    }    async getTranslatorCode() {      const t = (await z.getValue() || []).find(o => o.provider.toLowerCase() === this.serviceName.toLowerCase());      if (!t) return console.warn(`๐Ÿš€ ~ Service settings not found for: ${this.serviceName}, using default translator code`), "1";      if (!t.translatorItemList || t.translatorItemList.length === 0) return console.warn(`๐Ÿš€ ~ No translator items found for: ${this.serviceName}, using default translator code`), "1";      let a = t.translatorItemList.find(o => o.modelName === t.selectModel) || t.translatorItemList[0];      const n = async () => {        const o = await V.getValue();        return !o || typeof o.vipExpiryTimestamp > "u" || o.vipExpiryTimestamp === 0 ? !1 : o.vipExpiryTimestamp > Date.now()      }, s = a;      if (s && s.vipLevel === 1 && !await n()) {        console.warn(`๐Ÿš€ ~ User is not Pro but selected Pro model: ${a.modelName}, switching to free model`);        const o = t.translatorItemList.find(i => i.vipLevel === 0);        if (o) {          a = o;          const c = (await z.getValue()).map(h => h.provider.toLowerCase() === this.serviceName.toLowerCase() ? {            ...h,            selectModel: o.modelName          } : h);          await z.setValue(c), console.log(`๐Ÿš€ ~ Automatically switched to free model: ${o.modelName}`)        }      }      return !a || !a.modelCode ? (console.warn(`๐Ÿš€ ~ Selected translator not found for: ${this.serviceName}, using default translator code`), "1") : a.modelCode    }

Shared request wrapper adds plugin, version, timezone, and auth headers

background.js
    async request(e, t) {      const [a, n, s] = await Promise.all([K.getValue(), Ie.getValue(), V.getValue()]);      console.log("๐Ÿš€ ~ ApiService ~ request ~ baseUrl:", j.env.apiBaseUrl);      const o = typeof e == "string" && e.startsWith("/") ? j.env.apiBaseUrl + e : e,        {          timeout: i = -1,          parseData: c = !0,          cacheTTL: h = -1,          forceUpdateCache: f = !1,          ...w        } = t,        b = JSON.stringify({          url: o,          options: w        });      if (h > 0 && !f) {        const v = await this.cache.get(b);        if (v) return console.log("๐Ÿš€ ~ ApiService ~ request ~ cache hit:", {          ...JSON.parse(b),          cacheData: v        }), v      }      try {        const v = new AbortController,          R = i > 0 ? setTimeout(() => v.abort(), i) : null,          d = {            "Accept-Language": this.mapToApiLanguageCode(a),            "X-Plugins-Id": n,            "X-Browser-Language": chrome.i18n.getUILanguage(),            "X-Client-Version": chrome.runtime.getManifest().version,            "X-Timezone": Intl.DateTimeFormat().resolvedOptions().timeZone          };        s?.token && (d.Authorization = s.token), console.log("๐Ÿš€ ~ ApiService ~ request ~ commonHeaders", d);        const u = await fetch(o, {          ...w,          headers: {            "Content-Type": "application/json",            ...d,            ...w?.headers          },          signal: v.signal        });        if (R && clearTimeout(R), console.log("๐Ÿš€ ~ ApiService ~ request ~ response", u), !u.ok) throw new M({          source: "server",          message: u.statusText,          code: u.status        });        const m = await u.json();        if (m.code && m.code !== "S000000") throw m.code === "C000302" && (console.log("๐Ÿš€ ~ ApiService ~ C000302 error detected, showing VIP dialog"), this.handleVipUpgradeRequired()), new M({          source: "server",          message: m.message,          code: m.code        });        console.log("๐Ÿš€ ~ ApiService ~ request ~ res:", m);        const p = c ? m.data : m;        return h > 0 && this.cache.set(b, p, h), console.log("๐Ÿš€ ~ ApiService ~ request ~ success", {          url: o,          options: t,          result: p        }), p      } catch (v) {        throw console.error(`๐Ÿš€ ~ ApiService ~ request ${o} ~ error`, v), v.name === "AbortError" ? new M({          source: "network",          message: "Request timed out"        }) : new M({          source: "network",          message: v.message,          code: v.code,          traceId: v.traceId        })      }    }
Remote host receiving the translation request
    • deeptranslate.ai

    Receives page text chunks, page URL context, translation settings, plugin ID, client metadata, and signed-in authorization when present for the textTranslate proxy request.

Persistent UUID links every translation request to your browser across sessions

Every translation request includes a permanent random ID assigned at install.

With your timezone and language, this builds a cross-session history of what you translated, when, and from which browser, with no opt-in.

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

You visit a page and the extension translates it.

This fires automatically on navigation, no button press required.

The extension did this

The extension sends your permanent browser UUID to deeptranslate.ai with every API call.

The same UUID is attached whether you are logged in or not, allowing server-side linkage across sessions.

Captured request
POSThttps://deeptranslate.ai/trans/portal/translate/textTranslate

Translation result returned. UUID present on all 8 observed requests in the same session; value identical to chrome.storage.local.pluginsId.

Headers
X-Timezone
Europe/London
Content-Type
application/json
X-Plugins-Id
a299c53b-5e50-4f3b-8fc4-4b2d63b63f43
X-Client-Version
1.7.5
X-Browser-Language
en-US
Fingerprinting headers sent on every request
  • Permanent browser ID
    a299c53b-5e50-4f3b-8fc4-4b2d63b63f43

    A UUID generated once at install and never rotated. Ties every request you make across all sessions to a single browser identity.

  • Your timezone
    Europe/London

    Your local timezone, which narrows your geographic location and correlates with device settings.

  • Browser language
    en-US

    The UI language Chrome is set to on your device.

  • Extension version
    1.7.5

    The version of DeepTranslate installed, which also correlates with your Chrome update cadence.

The code that does this

UUID generated at install and sent on every request

Readable version

Install handler โ€” UUID creation

background.js:4759
chrome.runtime.onInstalled.addListener(async (details) => {  if (details.reason === 'install') {    // Generate a permanent UUID and store it on first install    chrome.storage.local.set({ pluginsId: self.crypto.randomUUID() });    ...  }});

Request builder โ€” four identifiers added to every API call

background.js:3299
const commonHeaders = {  'Accept-Language': this.mapToApiLanguageCode(acceptLanguage),  'X-Plugins-Id': pluginsId,            // permanent UUID from storage  'X-Browser-Language': chrome.i18n.getUILanguage(),  'X-Client-Version': chrome.runtime.getManifest().version,  'X-Timezone': Intl.DateTimeFormat().resolvedOptions().timeZone};
What's stored on your device

Written once at install, never changed. Matches the X-Plugins-Id header on every captured request, confirming reuse across sessions.

Location
chrome.storage.local key 'pluginsId'
Contents
a299c53b-5e50-4f3b-8fc4-4b2d63b63f43
Destination receiving the tracking UUID
    • deeptranslate.ai

    The developer's own translation API. Receives the UUID plus timezone, language, version on every translation, quota, user-info, and rule-fetch call.

Where it sends data

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

  • www.google-analytics.comwidely used

    DeepTranslate sends data to www.google-analytics.com. A widely used service: 346 other extensions we have analysed send data here.

  • deeptranslate.ai

    DeepTranslate sends data to deeptranslate.ai. No other extension we have analysed sends data here.

Updated 30 September 2026noofnfniimcnghnkonpdlhlaeaocidbi