Is Manga Translator safe?

High risk

Manga Translator is high 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.944Chrome Web Store
75Risk

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

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Screenshot translation uploads the active-tab image

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.

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

02EvidenceFIELD TABLE
Fields constructed for the screenshot translation request
FieldValueWhy it matters
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: trueTells 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.
03EvidenceNETWORK CAPTURE
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
Acceptapplication/json
Content-Typeapplication/json
04EvidenceCODE COMPARE
The code that does this

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

What it actually does
Click handler captures a PNG data URL and hands it to the translation callbackdist/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 screenshotdist/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 imagedist/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 createMangaByDataUrldist/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()
  }))
}
05EvidenceTHIRD PARTY LIST
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.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Tracked UI Clicks Post Event Metadata

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

The builder can serialize URL parts when supplied; the inspected handler omits URL.

Dynamic analysis saw no logEvent traffic.

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

02EvidenceFIELD TABLE
Fields assembled for the logEvent mutation
FieldValueWhy it matters
Event name
ClickDescribes the kind of interaction you made with the extension interface.
Clicked component
translateNowButtonIdentifies which Manga Translator control you clicked.
Container
controlPanelAdds the surrounding Manga Translator UI area for the clicked control when that metadata exists.
Visit identifier
8f2f3d0a-5f7a-4e4f-9a21-0e9c9817d118Lets the service connect the click event to the same extension visit session.
Optional URL fields
hostname=mangadex.org; pathname=/chapter/6f4c8f3b; query=?tab=commentsCan describe the page path, host, and query string if a URL is supplied to the tracking helper.
03EvidenceNETWORK CAPTURE
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
Acceptapplication/json
Content-Typeapplication/json
04EvidenceCODE COMPARE
The code that does this

Click metadata extraction and logEvent POST construction

What it actually does
Readable page-level click listenerdeobfuscated/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 helperdeobfuscated/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 helperdeobfuscated/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
    }
  })
}
05EvidenceTHIRD PARTY LIST
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.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

Persistent cross-session tracking UUID sent on every API call

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.

Sign-in triggers mergeTrackingId, linking anonymous history to your account.

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

02EvidenceNETWORK CAPTURE
Captured request
POSThttps://api.ismanga.com/graphql
Translation result returned. The UUID is logged server-side regardless of query type.
Headers
Acceptapplication/json
Content-Typeapplication/json
AuthorizationBearer eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qif...
ismanga-visit-id196c1ba2-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 } } }"
}
03EvidenceSTORAGE DUMP
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.

Locationchrome.storage.sync key 'visitId'
Contents (JSON)
{
  "visitId": "196c1ba2-c14c-49cd-afda-dfbe923edb30"
}
04EvidenceCODE COMPARE
The code that does this

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

What it actually does
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 });
  }
};
05EvidenceCODE COMPARE
The code that does this

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

What it actually does
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 }));
06EvidenceTHIRD PARTY LIST
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 17 September 2026oooikipggipaohjibkmoiacmmlilgokn