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.97Chrome Web Store
45Risk
Who publishes it

Ichigo LLC - no other listings under this identity, 2 shared hostnames

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

Publisher
Ichigo LLC
Website

Shared hosts - 2 hostnames

Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.

klmanga.com
Also called by 2 other listings, including KanFan
manhuagui.com
Also called by 5 other listings, including KanFan

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

Publishers can request a review.

Findings

Manga Translator sends a device fingerprint

MediumunwantedCWE-359Dynamic sandbox

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.

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.

Fields attached to translation traffic

  • Device fingerprint
    3f2a91c0b8a4d12e7c903f1a5e61b02a9d4c8f33

    Lets the service correlate translation requests from the same browser even when the page being translated changes.

  • Client ID
    9b3b3a8e-7d52-4e19-9b8c-91c6f9a2d4b7

    Gives 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
    en

    Shows 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.

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.

The code that does this

Fingerprint creation and request attachment in the shipped background bundle

Readable version

Readable fingerprint builder

deobfuscated/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 request

deobfuscated/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 request

deobfuscated/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]            }          }))        }

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.

Where it sends data

Destinations our analysis observed manga-translator%F0%9F%8D%93 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.

  • ichigoreader.com

    manga-translator%F0%9F%8D%93 sends data to ichigoreader.com. No other extension we have analyzed sends data here.

Updated 30 September 2026lepcfgkehgeiblekejomdmdklmjdmflp