Is KanFan - Manga Translator safe?

High risk

KanFan is high risk. When you click KanFan's "Allow and retry" prompt, the extension asks Chrome for cookie access, stores the origin, and later reads cookies for image URLs. DA confirmed a GET to api.kanfan.site/test with the cookie header present.

zshang2028v3.3.2Chrome Web Store
72Risk

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

Publishers can request a review.

Findings

Cookie headers added to image fetches after approval

When you click KanFan's "Allow and retry" prompt, the extension asks Chrome for cookie access, stores the origin, and later reads cookies for image URLs.

DA confirmed a GET to api.kanfan.site/test with the cookie header present.

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

You approve the cookie-assist prompt for a site.

The content script sends the current page URL to the background service worker.

The extension did this

The extension reads cookies for matching image URLs and adds them to retry requests.

The approval is saved per origin and reused until revoked.

Request and storage fields involved in cookie assist
  • Cookie request header
    sessionid=4f8c0f7a9d2e11ef; cf_clearance=R4k9QvM2h7 (illustrative)

    This can carry signed-in session state for the site whose image is being fetched.

  • Approved site origin
    https://www.pixiv.net

    This lets the extension remember which site you approved for future image retries.

  • Image referrer
    https://www.pixiv.net/

    This tells the image host which site context the retry is meant to resemble.

Captured request
GEThttps://api.kanfan.site/test

Cookie-assisted GET observed after approval; no request body was recorded.

The code that does this

The click prompt asks for cookie assist and saves approval state

Readable version

Content-script Allow button sends requestCookieAssist

content.js
C.addEventListener("click", async () => {              C.disabled = !0;              const t = await new Promise(e => {                try {                  chrome.runtime.sendMessage({                    action: i.REQUEST_COOKIE_ASSIST,                    pageUrl: location.href                  }, t => e(t))                } catch (t) {                  e({                    ok: !1,                    error: t?.message || String(t)                  })                }              });              if (C.disabled = !1, t?.ok) return T.style.display = "none", void("function" == typeof e && e(t));              _.textContent = (0, s.i18nMessage)("msgCookieAssistDenied", "Cookie access was not enabled."), C.style.display = "none"            })

Background handler requests cookies permission and stores the origin

background.js
async function(e, t, a) {            try {              const n = m(e, t);              if (!n) return void a({                ok: !1,                error: "invalid_origin"              });              if (!await S() && !await async function() {                  try {                    return !!chrome?.permissions?.request && await chrome.permissions.request({                      permissions: ["cookies"]                    })                  } catch {                    return !1                  }                }()) {                const e = await p();                return e.includes(n) || await A([...e, n]), void a({                  ok: !1,                  origin: n,                  error: "permission_denied"                })              }              const r = await _();              r.includes(n) || await w([...r, n]);              const o = await p();              o.includes(n) && await A(o.filter(e => e !== n)), a({                ok: !0,                origin: n,                allowed: !0,                hasCookiePermission: !0              })            } catch (e) {              a({                ok: !1,                error: e?.message || String(e)              })            }          }
The code that does this

The image retry path reads cookies and sets the Cookie header

Readable version

Readable fetchImageResponse implementation

background.js
t.fetchImageResponse = async function(e, t) {          const o = function(e) {              const t = new URL(e).hostname;              return a.find(e => t === e.urlFilter || t.endsWith(e.urlFilter))            }(e),            l = o?.referer;          let u = null;          try {            t && (u = new URL(t).href)          } catch {}          if (!u) try {            const e = await chrome.storage.local.get(["last_active_url"]);            e?.last_active_url && (u = new URL(e.last_active_url).href)          } catch {}          const d = new URL(e).origin + "/",            h = l || u || d,            f = new URL(e).hostname;          let g = h;          if (!l) try {            "same-origin" !== i(f, new URL(h).hostname) && (g = new URL(h).origin + "/")          } catch {}          let m = null;          try {            const e = new URL(h),              t = e.hostname.replace(/^www\./, "");            m = `${e.protocol}//${t}`          } catch {}          const _ = await async function(e, t) {            try {              const a = await chrome.storage.local.get([n]),                r = Array.isArray(a?.[n]) ? a[n] : [],                o = c(t),                s = c(e);              return !!(o && r.includes(o) || s && r.includes(s))            } catch {              return !1            }          }(e, t), p = !_ && await async function(e, t) {            try {              const a = await chrome.storage.local.get([r]),                n = Array.isArray(a?.[r]) ? a[r] : [],                o = c(t),                s = c(e);              return !!(o && n.includes(o) || s && n.includes(s))            } catch {              return !1            }          }(e, t), w = !!_ && await async function() {            try {              return !!chrome?.permissions?.contains && await chrome.permissions.contains({                permissions: ["cookies"]              })            } catch {              return !1            }          }(), A = _ && w ? await async function(e, t) {            try {              if (!chrome?.cookies?.getAll) return "";              const a = await chrome.cookies.getAll({                url: e              });              let n = [];              if (t) try {                n = await chrome.cookies.getAll({                  url: e,                  partitionKey: {                    topLevelSite: t                  }                })              } catch {}              const r = new Map;              for (const e of a) r.set(e.name, e);              for (const e of n) r.set(e.name, e);              return r.size ? Array.from(r.values()).map(e => `${e.name}=${e.value}`).join("; ") : ""            } catch {              return ""            }          }(e, m): "", S = await async function({            referer: e,            domain: t,            cookieHeader: a          }, n) {            const r = Math.floor(2e4 * Math.random()) + 1e3;            let o = "cross-site";            try {              o = i(t, new URL(e).hostname)            } catch {}            const s = [{              header: "referer",              operation: "set",              value: e            }, {              header: "sec-fetch-site",              operation: "set",              value: o            }, {              header: "sec-fetch-mode",              operation: "set",              value: "no-cors"            }, {              header: "sec-fetch-dest",              operation: "set",              value: "image"            }];            a && s.push({              header: "cookie",              operation: "set",              value: a            });            const c = {              id: r,              priority: 1,              action: {                type: "modifyHeaders",                requestHeaders: s              },              condition: {                urlFilter: `||${t}`,                resourceTypes: ["xmlhttprequest", "image", "other"]              }            };            try {              return await chrome.declarativeNetRequest.updateDynamicRules({                addRules: [c],                removeRuleIds: [r]              }), await n()            } catch (e) {              throw console.error(`[DNR] Rule add failed (ID:${r}):`, e), e            } finally {              try {                await chrome.declarativeNetRequest.updateDynamicRules({                  addRules: [],                  removeRuleIds: [r]                })              } catch (e) {                console.warn("[DNR] Rule cleanup failed:", e)              }            }          }({            referer: g,            domain: f,            cookieHeader: A          }, () => fetch(e, {            method: "GET",            credentials: "omit",            cache: "no-cache",            headers: {              Accept: "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"            }          }));          if (!(S?.ok || A || p || _ && w)) throw new s;          return S        };
Hosts involved in the cookie-assisted fetch evidence
    • api.kanfan.site

    Observed endpoint for the confirmed cookie-assisted GET request during verification.

    • pximg.net

    Hardcoded image-host rule that uses https://www.pixiv.net/ as the referrer context.

    • hamreus.com

    Hardcoded image-host rule that uses https://www.manhuagui.com/ as the referrer context.

    • klimv1.xyz

    Hardcoded image-host rule that uses https://klmanga.com/ as the referrer context.

Updated 30 September 2026ffjpelbfmecoidnkjapdhpcbeianmkpp