Is Manga Translator safe?

Medium risk

Manga Translator is medium risk. Clicking Manga Translator's screenshot button makes the content script capture the area via the service worker and send the PNG data URL to api.ismanga.com/graphql, with the page URL, flagged as a screenshot translation.…

ismangav0.2.949Chrome Web Store
49Risk
Who publishes it

ismanga - no other listings under this identity

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

Publisher
ismanga

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

Publishers can request a review.

Findings

Screenshot translation uploads the active-tab image

HighunwantedCWE-359Dynamic sandbox

Clicking Manga Translator's screenshot button makes the content script capture the area via the service worker and send the PNG data URL to api.ismanga.com/graphql, with the page URL, flagged as a screenshot translation.

What actually happens

You did this

You click the screenshot translation button in the extension overlay.

The button is rendered as clickToTranslateButton in the content script.

The extension did this

The extension captures the visible tab image and submits it with the page URL.

The request goes to the Manga Translator GraphQL API for screenshot-based translation.

Fields constructed for the screenshot translation request

  • Visible page image
    data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII= (illustrative)

    Lets the translation service see the portion of the page captured for screenshot translation.

  • Current page URL
    https://mangadex.org/chapter/7f3b0c2e-1a3d-4c5b-98f0-23a2b41c9d75/12 (illustrative)

    Ties the screenshot to the page you were viewing when you asked for translation.

  • Screenshot marker
    isScreenshot: true

    Tells the service that this upload came from screenshot translation rather than URL translation.

  • Device identifier
    1d7d2df7-65d7-41e2-8f41-8a6e6b1c2d49 (illustrative)

    Can link multiple translation requests from the same browser profile over time.

Captured request

POSThttps://api.ismanga.com/graphql

Dynamic analysis did not reach the screenshot button, so no captured request body or response is available.

Headers
Accept
application/json
Content-Type
application/json

The code that does this

The overlay click leads to a service-worker screenshot and GraphQL upload

Readable version

Click handler captures a PNG data URL and hands it to the translation callback

dist/content.bundle.js
onClick: function() {  var t = (0, e.A)(s().mark((function e(t) {    var n, a, r, o;    return s().wrap((function(e) {      for (;;) switch (e.prev = e.next) {        case 0:          if (t.preventDefault(), t.stopPropagation(), w) {            e.next = 1;            break          }          return window.open("".concat(k.ISMANGA_SUBSCRIPTION_PRICING_ENDPOINT, "/").concat((null == p ? void 0 : p.uid) || ""), "_blank"), e.abrupt("return");        case 1:          if (b.current && d && f) {            e.next = 2;            break          }          return e.abrupt("return");        case 2:          return b.current.style.visibility = "hidden", e.next = 3, (0, g.getTargetDataUrl)({            targetElement: d,            screenshotViewport: f          });        case 3:          return n = e.sent, a = (0, y.getDataUrlId)(n), r = Boolean, e.next = 4, c({            imgDataUrl: n,            imgDataUrlId: a          });        case 4:          o = e.sent, r(o) && (b.current.style.visibility = "visible");        case 5:        case "end":          return e.stop()      }    }), e)  })));  return function(e) {    return t.apply(this, arguments)  }}()

Content script asks the service worker to take a screenshot

dist/content.bundle.js
var ml = function() {  var e = t(r().mark((function e() {    var t, n;    return r().wrap((function(e) {      for (;;) switch (e.prev = e.next) {        case 0:          return e.next = 2, chrome.runtime.sendMessage({            code: "take_screenshot"          });        case 2:          return t = e.sent, n = t.payload, e.abrupt("return", n);        case 5:        case "end":          return e.stop()      }    }), e)  })));  return function() {    return e.apply(this, arguments)  }}()

Service worker captures the active tab image

dist/background.bundle.js
chrome.runtime.onMessage.addListener((function(e, t, r) {  return "take_screenshot" === e.code && (n(s().mark((function e() {    var t;    return s().wrap((function(e) {      for (;;) switch (e.prev = e.next) {        case 0:          return e.next = 2, chrome.tabs.captureVisibleTab();        case 2:          t = e.sent, r({            code: xr,            payload: t          });        case 4:        case "end":          return e.stop()      }    }), e)  })))(), !0)}))

Screenshot data URL is placed into createMangaByDataUrl

dist/content.bundle.js
function(e) {  var t = e.imgDataUrl,    n = e.imgDataUrlId,    a = e.originUrl,    r = e.toLang,    s = void 0 === r ? o.LANG_CODE.EN_US : r,    l = e.fromLang,    u = e.isScreenshot,    c = void 0 !== u && u,    d = e.shouldSkipCache,    p = void 0 !== d && d,    f = e.shouldForceRefresh,    m = void 0 !== f && f,    g = e.idToken,    h = e.visitId,    b = e.deviceId,    y = JSON.stringify({      query: 'mutation {\n                    createMangaByDataUrl(\n                        dataUrl: "'.concat(t, '",\n                        dataUrlId: "').concat(n, '",\n                        originUrl: "').concat(a, '",\n                        langCode: "').concat(s, '",\n                        selectedLangCode: "').concat(l, '",\n                        isScreenshot: ').concat(c, ",\n                        shouldSkipCache: ").concat(p, ",\n                        shouldForceRefresh: ").concat(m, "\n                    ) { \n                        code\n                        payload {\n                            x, \n                            y, \n                            width \n                            height \n                            fromLang {\n                                code\n                                script\n                            }\n                            toLang {\n                                code\n                                script\n                            }\n                            imgWidth\n                            imgHeight\n                            inpaintingUrl\n                            colors {\n                                text\n                                background\n                            }\n                        }\n                        taskPayload {\n                            taskId\n                            url\n                            isPseudoUrl\n                            isHighQuality\n                            langCode\n                            shouldCache\n                        }\n                    }\n                }\n            ")    });  return (0, i.post)({    idToken: g,    visitId: h,    deviceId: b,    init: {      body: y    }  }).then((function(e) {    return e.json()  }))}

External service receiving the screenshot translation request

    • api.ismanga.com

    Receives the GraphQL createMangaByDataUrl request containing the screenshot PNG data URL, current page URL, language settings, and request metadata.

Tracked UI Clicks Post Event Metadata

MediumunwantedCWE-359Dynamic sandbox

Manga Translator attaches click tracking to UI elements and posts a logEvent mutation to api.ismanga.com/graphql.

What actually happens

You did this

You click a Manga Translator UI control on a page.

The click must occur on an element, or a parent element, with Manga Translator component metadata.

The extension did this

The extension prepares a GraphQL event record and posts it to Manga Translator's API.

The record includes the click event name, component, container, platform, and visit identifier; URL slots are populated only if the caller supplies a URL.

Fields assembled for the logEvent mutation

  • Event name
    Click

    Describes the kind of interaction you made with the extension interface.

  • Clicked component
    translateNowButton

    Identifies which Manga Translator control you clicked.

  • Container
    controlPanel

    Adds the surrounding Manga Translator UI area for the clicked control when that metadata exists.

  • Visit identifier
    8f2f3d0a-5f7a-4e4f-9a21-0e9c9817d118

    Lets the service connect the click event to the same extension visit session.

  • Optional URL fields
    hostname=mangadex.org; pathname=/chapter/6f4c8f3b; query=?tab=comments

    Can describe the page path, host, and query string if a URL is supplied to the tracking helper.

Captured request

POSThttps://api.ismanga.com/graphql

Dynamic analysis observed GraphQL traffic to this endpoint, but did not capture a logEvent mutation after popup clicks; the page overlay controls were not successfully clicked.

Headers
Accept
application/json
Content-Type
application/json

The code that does this

Click metadata extraction and logEvent POST construction

Readable version

Readable page-level click listener

deobfuscated/dist/content.bundle.js
window.addEventListener("click", (function(e) {  var t;  t = {    event: e,    visitId: n,    idToken: l  }, (0, Da.handleClickTracking)(function(e) {    for (var t = 1; t < arguments.length; t++) {      var n = null != arguments[t] ? arguments[t] : {};      t % 2 ? lr(Object(n), !0).forEach((function(t) {        Ca(e, t, n[t])      })) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n)) : lr(Object(n)).forEach((function(t) {        Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t))      }))    }    return e  }({    platform: "extension"  }, t))}), {  capture: !0,  passive: !0})

Readable component lookup and click tracking helper

deobfuscated/dist/content.bundle.js
i = function(e, t) {  for (var n = e instanceof HTMLElement ? e : null; n;) {    var a, r = null === (a = n.dataset) || void 0 === a ? void 0 : a[t];    if (r) return r;    n = n.parentElement  }  return null};s = function(n) {  var a = n.platform,    r = n.event,    o = n.idToken,    s = n.visitId,    l = n.url,    u = i(r.target, "ismangaComponent");  if (u) {    var c = i(r.target, "ismangaContainer"),      d = function(e) {        if (e) {          if ("string" != typeof e) return e;          try {            var t = new URL(e);            return {              pathname: t.pathname,              hostname: t.hostname,              query: t.search            }          } catch (e) {            return          }        }      }(l);    (0, e.default)({      eventName: t.TRACKING_EVENTS.CLICK,      idToken: o,      visitId: s,      component: u,      container: null != c ? c : "",      platform: null != a ? a : "",      url: d    })  }}

Readable logEvent mutation helper

deobfuscated/dist/content.bundle.js
function(t) {  var n, a, r, s, l, u, c, d, p, f, m, g;  n = t.eventName;  a = t.url;  r = t.component;  s = t.container;  l = t.idToken;  u = t.visitId;  c = t.platform;  d = t.extra;  p = void 0 === d ? "" : d;  f = t.sessionId;  m = t.sessionDurationSeconds;  g = JSON.stringify({    query: 'mutation {\n                logEvent(\n                    platform: "'.concat(c, '", \n                    url: {\n                        pathname: "').concat(null == a ? void 0 : a.pathname, '",\n                        hostname: "').concat(null == a ? void 0 : a.hostname, '",\n                        query: "').concat(null == a ? void 0 : a.query, '"\n                    },\n                    eventName: "').concat(n, '",\n                    component: "').concat(r, '",\n                    container: "').concat(s, '",\n                    extra: "').concat(p, '"\n                    ').concat(f ? ", sessionId: ".concat(f) : "", "\n                    ".concat(m ? ", sessionDurationSeconds: ".concat(m) : "", "\n                ) {\n                success\n                message\n            }\n        }")  });  return (0, i.post)({    visitId: u,    idToken: l,    init: {      body: g    }  })}

Remote destination used by this code path

    • api.ismanga.com

    Receives Manga Translator GraphQL requests, including logEvent when tracked UI controls call the event helper.

Persistent cross-session tracking UUID sent on every API call

MediumunwantedCWE-200Dynamic sandbox

Dynamic analysis captured 25 POSTs to api.ismanga.com/graphql in two sessions, each with the same ismanga-visit-id header, a UUID stored in chrome.storage.sync.

What actually happens

You did this

You install the extension and visit any web page containing manga images.

The extension's content script is injected on all URLs.

The extension did this

The extension attaches a permanent UUID to every POST request it sends to its translation server.

The ismanga-visit-id header carries the same value on every request, in every tab, across every browsing session.

Captured request

POSThttps://api.ismanga.com/graphql

Translation result returned. The UUID is logged server-side regardless of query type.

Headers
Accept
application/json
Content-Type
application/json
Authorization
Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qif...
ismanga-visit-id
196c1ba2-c14c-49cd-afda-dfbe923edb30
Body
{  "query": "query { pollTranslationQueue(taskId: \"abc123\", isHighQuality: false, url: \"https://example-manga-site.com/chapter/1/page/5.jpg\", isPseudoUrl: false, langCode: \"en\", shouldCache: true) { code payload { x y width height } } }"}

What's stored on your device

The extension's permanent device identifier. Never cleared or rotated; syncs across every Chrome profile on the same Google account.

Location
chrome.storage.sync key 'visitId'
Contents
{  "visitId": "196c1ba2-c14c-49cd-afda-dfbe923edb30"}

The code that does this

UUID generation and persistence (content.bundle.js ~line 125139-125167)

Readable version
var getOrCreateVisitId = async function(callback) {  try {    const stored = await chrome.storage.sync.get([VISIT_ID_KEY]);    let visitId = stored[VISIT_ID_KEY];    if (!visitId) {      visitId = uuidv4();      chrome.storage.sync.set({ [VISIT_ID_KEY]: visitId });    }    await callback?.(visitId);  } catch (err) {    throw new Error('Failed to get visitId', { cause: err });  }};

The code that does this

Anonymous-to-account linking on sign-in (content.bundle.js ~line 26979-26991)

Readable version
var mergeTrackingId = function({ idToken, visitId }) {  const body = JSON.stringify({    query: `mutation {      mergeTrackingId {        success      }    }`  });  return post({ visitId, idToken, init: { body } });};// Called immediately after successful email or Google sign-in:// ur((visitId) => mergeTrackingId({ idToken, visitId }));

Destinations receiving the persistent UUID

    • api.ismanga.com

    IsManga's GraphQL API. Receives every translation request and analytics event with the ismanga-visit-id UUID and, for signed-in users, a Firebase ID token.

Updated 2 November 2026oooikipggipaohjibkmoiacmmlilgokn