Is Manga Translator🍓 safe?

Medium risk

Manga Translator sends a device fingerprint and base64-encoded image data to ichigoreader.com with every translation request.

When a user triggers image translation, the extension captures the target image as base64 and transmits it to ichigoreader.com/translate along with a detailed hardware fingerprint. The fingerprint includes WebGL renderer info, canvas hash, screen dimensions, hardware concurrency, device memory, connection type, locale, and other browser capability flags. Both the fingerprint and image data are sent on every translation call, not just at install time.

Ichigo LLCv0.0.95Chrome 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

Manga Translator sends a device fingerprint

Translating an image builds a fingerprint from your renderer, screen, hardware, locale, capabilities, touch/orientation, user-agent, and JS heap signals, sent to ichigoreader.com/translate and also as a query parameter to /metrics.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You enable translation on a page or request an image translation.

The content script then finds eligible page images and converts them into translation jobs.

The extension did this

The extension computes a device fingerprint and attaches it to translation traffic.

The same fingerprint function is referenced by the translation POST and the metrics GET.

02EvidenceFIELD TABLE
Fields attached to translation traffic
FieldValueWhy it matters
Device fingerprint
3f2a91c0b8a4d12e7c903f1a5e61b02a9d4c8f33Lets the service correlate translation requests from the same browser even when the page being translated changes.
Client ID
9b3b3a8e-7d52-4e19-9b8c-91c6f9a2d4b7Gives the service another stable identifier for the same extension install.
Image being translated
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=Sends the selected page image to the translation service so text can be extracted and translated.
Target language
enShows which translation language you requested for the image.
Network address
198.51.100.23 (illustrative)The server receiving the request can associate the fingerprint with the connection used for translation.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://ichigoreader.com/translate
Confirmed during dynamic analysis: the POST body contained a fingerprint field. The retained evidence did not include a full body copy.
04EvidenceCODE COMPARE
The code that does this

Fingerprint creation and request attachment in the shipped background bundle

What it actually does
Readable fingerprint builderdeobfuscated/js/background.js
t.getFingerprint = function() {
          if (d) return d;
          const e = function() {
              try {
                if (s) {
                  const e = s.createElement("canvas").getContext("webgl");
                  if (!e) return "no-webgl";
                  const t = e.getExtension("WEBGL_debug_renderer_info");
                  return t ? e.getParameter(t.UNMASKED_RENDERER_WEBGL) : "unknown"
                }
                if ("undefined" != typeof OffscreenCanvas) {
                  const e = new OffscreenCanvas(1, 1).getContext("webgl");
                  if (!e) return "no-webgl";
                  const t = e.getExtension("WEBGL_debug_renderer_info");
                  return t ? e.getParameter(t.UNMASKED_RENDERER_WEBGL) : "unknown"
                }
              } catch (e) {
                return "webgl-error"
              }
              return "webgl-unavailable"
            }(),
            t = `${null!==(w=null==l?void 0:l.hardwareConcurrency)&&void 0!==w?w:"unknown"}-${null!==(b=null==l?void 0:l.deviceMemory)&&void 0!==b?b:"unknown"}`,
            n = function() {
              var e, t, n, o, r;
              const i = null == l ? void 0 : l.connection;
              return `${null!==(e=null==i?void 0:i.type)&&void 0!==e?e:"unknown"}-${null!==(t=null==i?void 0:i.rtt)&&void 0!==t?t:"unknown"}-${null!==(n=null==i?void 0:i.downlinkMax)&&void 0!==n?n:"unknown"}-${null!==(o=null==i?void 0:i.effectiveType)&&void 0!==o?o:"unknown"}-${null!==(r=null==i?void 0:i.saveData)&&void 0!==r&&r}`
            }(),
            o = (new Date).getTimezoneOffset(),
            r = function() {
              var e;
              const t = u;
              if (!t) return "screen-unavailable";
              return `${t.width}x${t.height}-${t.colorDepth}-${t.pixelDepth}-${t.availWidth}x${t.availHeight}-${null!==(e=null==a?void 0:a.devicePixelRatio)&&void 0!==e?e:1}`
            }(),
            f = function() {
              try {
                if (!s) return "canvas-unavailable";
                const e = s.createElement("canvas"),
                  t = e.getContext("2d");
                if (!t) return "no-2d-context";
                t.textBaseline = "top", t.font = "14px Arial", t.fillStyle = "rgba(102, 204, 0, 0.7)", t.fillRect(125, 1, 62, 20), t.fillStyle = "#f60", t.fillText("BrowserLeaks,com <canvas> 1.0", 2, 15), t.fillStyle = "rgba(102, 204, 0, 0.7)", t.fillText("BrowserLeaks,com <canvas> 1.0", 4, 17);
                const n = e.toDataURL();
                let o = 0;
                for (let e = 0; e < n.length; e++) o = (o << 5) - o + n.charCodeAt(e), o |= 0;
                return o.toString()
              } catch (e) {
                return "canvas-error"
              }
            }(),
            g = function() {
              const e = [],
                t = l,
                n = a;
              return t && "serviceWorker" in t && e.push("sw"), a && "localStorage" in a && e.push("ls"), a && "sessionStorage" in a && e.push("ss"), "indexedDB" in i && e.push("idb"), t && "geolocation" in t && e.push("geo"), "Notification" in i && e.push("notif"), t && "permissions" in t && e.push("perm"), "boolean" == typeof(null == t ? void 0 : t.cookieEnabled) && t.cookieEnabled && e.push("cookie"), void 0 !== (null == t ? void 0 : t.doNotTrack) && e.push("dnt"), t && "onLine" in t && e.push("online"), t && "deviceMemory" in t && e.push("devmem"), t && "connection" in t && e.push("conn"), "function" == typeof(null == t ? void 0 : t.getBattery) && e.push("bat"), t && "mediaDevices" in t && e.push("media"), n && "webkitRequestFileSystem" in n && e.push("fs"), n && "webkitStorageInfo" in n && e.push("storage"), e.sort().join(",")
            }(),
            v = function() {
              var e, t, n, o;
              const r = (null == l ? void 0 : l.languages) && l.languages.length > 0 ? l.languages : (null == l ? void 0 : l.language) ? [l.language] : [];
              return `${null!==(e=null==l?void 0:l.language)&&void 0!==e?e:"unknown"}-${null!==(t=null==l?void 0:l.userLanguage)&&void 0!==t?t:"unknown"}-${null!==(n=null==l?void 0:l.browserLanguage)&&void 0!==n?n:"unknown"}-${null!==(o=null==l?void 0:l.systemLanguage)&&void 0!==o?o:"unknown"}-${r.join(",")}`
            }(),
            m = function() {
              var e;
              const t = a;
              return `${null!==(e=null==l?void 0:l.maxTouchPoints)&&void 0!==e?e:0}-${!(!t||!("ontouchstart"in t))}-${"TouchEvent"in i}-${"PointerEvent"in i}`
            }(),
            h = function() {
              var e, t, n, o, r;
              const i = a;
              return `${null!==(t=null===(e=null==u?void 0:u.orientation)||void 0===e?void 0:e.type)&&void 0!==t?t:"unknown"}-${null!==(r=null!==(o=null===(n=null==u?void 0:u.orientation)||void 0===n?void 0:n.angle)&&void 0!==o?o:null==i?void 0:i.orientation)&&void 0!==r?r:0}-${!(!a||!("onorientationchange"in a))}`
            }(),
            p = function() {
              var e, t, n, o, r;
              const i = null !== (e = null == l ? void 0 : l.userAgent) && void 0 !== e ? e : "unknown";
              return `${null!==(t=null==l?void 0:l.platform)&&void 0!==t?t:"unknown"}-${null!==(n=null==l?void 0:l.vendor)&&void 0!==n?n:""}-${!!(null==l?void 0:l.cookieEnabled)}-${"function"==typeof(null==l?void 0:l.javaEnabled)&&l.javaEnabled()}-${/iPad|iPhone|iPod/.test(i)}-${/Safari/.test(i)&&!/Chrome/.test(i)}-${(null===(o=i.match(/Version\/(\d+)/))||void 0===o?void 0:o[1])||"unknown"}-${(null===(r=i.match(/(iPhone|iPad|iPod)/))||void 0===r?void 0:r[1])||"unknown"}`
            }(),
            y = [e, t, n, o, r, f, g, v, m, h, p, function() {
              try {
                const e = null != c ? c : null == a ? void 0 : a.performance;
                if (!e) return "no-performance";
                const t = e.timing,
                  n = e.memory,
                  o = null == l ? void 0 : l.connection,
                  r = (null == t ? void 0 : t.navigationStart) || 0,
                  i = ((null == t ? void 0 : t.loadEventEnd) || 0) - r,
                  s = (null == n ? void 0 : n.usedJSHeapSize) || 0,
                  u = (null == n ? void 0 : n.totalJSHeapSize) || 0,
                  d = (null == n ? void 0 : n.jsHeapSizeLimit) || 0,
                  f = (null == o ? void 0 : o.effectiveType) || "unknown";
                return `${i}-${s}-${u}-${d}-${f}-${(null==o?void 0:o.downlink)||0}-${(null==o?void 0:o.rtt)||0}`
              } catch (e) {
                return "performance-error"
              }
            }()].join("-");
          var w, b;
          return d = function(e) {
            let t = 5381;
            for (let n = 0; n < e.length; n++) t = 33 * t ^ e.charCodeAt(n);
            let n = (t >>> 0).toString(16).padStart(8, "0");
            const o = Math.ceil(e.length / 4);
            for (let t = 0; t < 4; t++) {
              const r = e.slice(t * o, (t + 1) * o);
              let i = 5381;
              for (let e = 0; e < r.length; e++) i = 33 * i ^ r.charCodeAt(e);
              n += (i >>> 0).toString(16).padStart(8, "0")
            }
            return n
          }(y), d
        }
Readable metrics requestdeobfuscated/js/background.js
t.getCurrentUser = function() {
          return o(this, void 0, void 0, (function*() {
            yield u();
            const e = yield r.appConfig.getClientUuid(), n = f(), o = yield fetch(`${t.baseUrl}/metrics?clientUuid=${e}&fingerprint=${(0,i.getFingerprint)()}`, {
              method: "GET",
              headers: n
            });
            if (o.status !== s.Ok) throw new Error("Failed to retrieve user.");
            return yield o.json()
          }))
        }
Readable translation requestdeobfuscated/js/background.js
t.translateImage = function(e, n, a) {
          return o(this, void 0, void 0, (function*() {
            const o = yield r.appConfig.getClientUuid(), l = yield fetch(`${t.baseUrl}/translate`, {
              method: "POST",
              headers: f(),
              body: JSON.stringify({
                base64Images: [n],
                translationModel: a,
                targetLangCode: e,
                fingerprint: (0, i.getFingerprint)(),
                clientUuid: o
              })
            });
            if (l.status === s.InternalServerError) {
              const e = "Server is down or experiencing issues. Sorry for the inconvenience.";
              return {
                errorMessage: e,
                translations: [{
                  originalLanguage: "Unknown",
                  translatedText: e,
                  minX: 0,
                  minY: 0,
                  maxX: 200,
                  maxY: 200
                }]
              }
            }
            if (l.status === s.TooManyRequests) {
              const e = "Out of translations. Server costs are expensive. Upgrade for more!";
              return {
                errorMessage: e,
                translations: [{
                  originalLanguage: "Unknown",
                  translatedText: e,
                  minX: 0,
                  minY: 0,
                  maxX: 200,
                  maxY: 200
                }]
              }
            }
            return {
              translations: (yield l.json()).images[0]
            }
          }))
        }
05EvidenceTHIRD PARTY LIST
Hosts receiving the fingerprint
  • ichigoreader.com

    Receives translation POSTs at /translate with fingerprint and clientUuid fields; the code also sends the fingerprint in the /metrics query string.

Data recipients

ichigoreader.com
Updated 17 September 2026lepcfgkehgeiblekejomdmdklmjdmflp