Is PikPak Browser Extension safe?

High risk

PikPak is high risk. PikPak Browser Extension injects its analytics SDK into non-Chrome tabs after tab updates. The SDK fires an image-beacon to rcv5-sp.mypikpak.com with an encrypted log built from page URL, title, referrer, and browser/screen details.…

PIKCLOUD PTE.LTDv1.9.0Chrome Web Store
75Risk
Who publishes it

PIKCLOUD PTE. LTD. - 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
PIKCLOUD PTE.LTD
Declared legal entity
PIKCLOUD PTE. LTD.
Registered contact
LIN FENG

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.

mypikpak.com
Also called by 1 other listing: 低端影视豆瓣助手
galaxystore.samsung.com
Also called by 2 other listings, including shoopit - pay less for travel

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

Publishers can request a review.

Findings

Every page URL is sent to PikPak analytics

PikPak Browser Extension injects its analytics SDK into non-Chrome tabs after tab updates.

The SDK fires an image-beacon to rcv5-sp.mypikpak.com with an encrypted log built from page URL, title, referrer, and browser/screen details.

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

You open or navigate a web page in a tab.

The extension did this

The extension injects an analytics SDK that reads page context and sends an encrypted beacon to rcv5-sp.mypikpak.com.

Captured request
GEThttps://rcv5-sp.mypikpak.com/api/stat/rt/js

Image beacon request observed during dynamic analysis; no response body was recorded.

Fields the injected SDK places into the encrypted analytics log
  • Current page URL
    https://account.example.com/settings?tab=security (illustrative)

    Shows the exact page you were viewing when the tab update happened.

  • Page title
    Account Security Settings (illustrative)

    Can reveal the name or topic of the page even when the address is less descriptive.

  • Referrer
    https://search.example.com/results?q=cloud+storage (illustrative)

    Shows the page that led to the current page when the browser exposes it.

  • Tracking ID
    8b1a9953c4611296a827abf8c47804d7 (illustrative)

    Lets repeated page views from the same browser be linked together.

  • Browser and screen details
    Chrome/126 on 1920x1080, 24-bit color, en-US (illustrative)

    Adds device context such as browser version, screen size, color depth, and language.

The code that does this

The service worker injects the analytics SDK, which encrypts and sends the page context

Readable version

Tab-update listener and readable injection handler

deobfuscated/background.js
chrome.tabs.onUpdated.addListener(r.Pc)const L = async (e, a, t) => {  try {    if (!a.status || t.url?.startsWith("chrome")) return;    await chrome.scripting.executeScript({      target: {        tabId: e      },      files: ["content.js", "ppa.min.js"]    })  } catch (e) {    console.log(e)  }}

Readable analytics configuration and collected fields

deobfuscated/ppa.min.js
var r = n(2),  i = t.exports = {    v: "2.0.1",    furl: "//rcv5-sp.mypikpak.com/api/stat/rt/js",    appid: "30211",    s: "57b07219c2aa11fe1e327dedb0bf8742",    comm: {      ai: null,      dm: window.realdomain || document.domain,      ul: document.URL,      tt: document.title,      sn: window.screen.width + "x" + window.screen.height,      cd: window.screen.colorDepth,      rr: document.referrer,      rf: r.getReferfrom() || "",      ua: navigator.userAgent,      lg: navigator.language,      ci: r.getCookieId(),      ed: {}    },    extend: function(t) {      return i.comm.ed = r.deepExtend({}, i.comm.ed, t), i.comm    },    config: function(t, e, n, i) {      this.appid = t, this.comm.ai = t, this.s = e, "boolean" == typeof i && (this.force = i), n && (this.event = r.unique(this.event.concat(n)))    },    event: ["click"],    debug: r.getParameter("debug"),    force: !0  }

Readable encrypted image-beacon sender

deobfuscated/ppa.min.js
function r(t) {  s.imageBeacon(t, function() {    o.debug && s.print.msg("Report success: " + t + ":)")  }, function() {    o.debug && s.print.err("Report error: " + t + ":(")  })}var i = n(3),  o = n(1),  s = n(2),  a = "JSON" in window ? JSON : n(71),  u = [],  c = function(t) {    var e, n, r, u = o.furl;    e = 1 === t.length ? s.deepExtend({}, o.comm, t[0]) : s.deepExtend({}, o.comm, {      il: t    }), e = a.stringify(e);    var c = i("mypikpak"),      f = c.slice(6, 22);    const h = s.aesEncrypt(e, c, f),      l = a.stringify({        _crypt_data: h      });    return n = "appId=" + o.appid + "log=" + l + "v=" + o.v, r = i(n + o.s), u + "?appId=" + o.appid + "&log=" + encodeURIComponent(l) + "&v=" + o.v + "&sig=" + r  },  f = "";t.exports = function(t) {  var e;  e = !1 === t.force ? t.force : o.force, delete t.force, u.push(s.deepExtend({}, t, {    tm: Math.round((new Date).getTime() / 1e3)  })), o.debug && s.print.msg("Report has been push into queue: " + a.stringify(t));  var n = c(u);  n.length > s.getBroswer() ? 1 === u.length ? (s.print.err("Report error: your single report is too long."), u = []) : (r(f), u.splice(0, u.length - 1), e && r(c(u))) : e ? (r(n), u = []) : f = n}

Tracking ID, beacon, and AES helpers

deobfuscated/ppa.min.js
getCookieId: function() {  if (!window.cookieId) {    var t = Math.random(),      e = navigator.appName + "_" + navigator.appVersion + "_" + navigator.userAgent + "_" + navigator.appCodeName + "_" + navigator.platform,      n = (new Date).valueOf();    window.cookieId = r(n + e + t)  }  return window.cookieId},imageBeacon: function(t, e, n) {  t = /^\/\//.test(t) ? "https:" + t : t;  var r = new Image;  r.onload = e, r.onerror = n, r.src = t},aesEncrypt: function(t, e, n) {  const r = s.createCipheriv("aes-256-cbc", e, n);  return r.update(t, "utf8", "hex") + r.final("hex")}
External analytics destination
    • rcv5-sp.mypikpak.com

    Receives the encrypted analytics beacon generated by the injected ppa.min.js SDK for PikPak.

PikPak Reads Clipboard Text on Copy

Copying text on a website triggers PikPak's content script to read the full clipboard text, if clipboard-analysis is on (shipped default).

The code checks if the text looks like a magnet, torrent, media, or installer link before a prompt.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You copy text on a webpage.

The extension has injected its content script into regular pages through its broad host permission.

The extension did this

PikPak reads the clipboard text and checks whether it looks like a downloadable link.

If a download-looking value matches, it opens a PikPak save prompt; no clipboard-content upload endpoint was identified for this claim.

Clipboard values read before local filtering
  • Full clipboard text
    magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567&dn=ubuntu-24.04-desktop-amd64.iso (illustrative)

    The extension reads the complete clipboard text when the copy handler runs, before deciding whether it's useful to PikPak.

  • Download-link match
    https://archive.org/download/sample-video-file/sample.mp4 (illustrative)

    The extension keeps values that look like magnet links, torrent files, media files, archives, installers, or similar downloadable URLs.

The code that does this

Clipboard analysis defaults on; the content script reads the clipboard inside the copy handler.

Readable version

Default options enable clipboard analysis

content.js
const k = Object.freeze({  [s.MAIN_CONTROL]: !0,  [s.INSERT_BUTTON_CONDITION]: i.AUTO,  [s.SELECTED_INSERT_BUTTON_SETTING]: !1,  [s.ANALYZE_CLIPBOARD_CONTENT]: !0,  [s.ANALYZE_BROWSER_DOWNLOAD_CONTENT]: !0,  [s.LAST_SELECT_FOLDER]: "My Links",  [s.MEDIA_INSERT_BUTTON_SETTING]: !0,  [s.MEDIA_INSERT_BUTTON_CONDITION]: o.AUTO,  [s.SHOW_FLOAT_BALL]: r.AS_NEED})

Download-link matcher used after the clipboard read

content.js
class oe extends ee {  static {    this.SOURCE_LINK_REGEX = /((magnet:\?|ftp:\/\/|thunder:\/\/|ed2k:\/\/)\S{3,})|((magnet:\?|https?:\/\/|ftp:\/\/|thunder:\/\/|ed2k:\/\/)[^=?&\s]+\.(torrent|mp4|rar|7z|zip|rmvb|mkv|avi|iso|exe|dmg))/gi  }  constructor() {    super(n.SELECTION), this.hide()  }  static computedTopRightOfSelectionRange(e) {    const n = Array.from(e),      t = Math.min(...n.map(e => e.top)),      a = Math.max(...n.filter(e => e.top === t).map(e => e.right));    return {      top: Math.max(window.scrollY + t - this.ICON_SIZE, 0),      left: a - this.ICON_SIZE    }  }  static init() {    const e = new this,      n = n => {        te(n) || e.hide()      },      t = n => {        te(n) || e.show()      };    return {      enable() {        e.show(), document.addEventListener("mousedown", n), document.addEventListener("mouseup", t)      },      disable() {        document.removeEventListener("mousedown", n), document.removeEventListener("mouseup", t)      }    }  }  setPosition(e) {    const {      top: n,      left: t    } = oe.computedTopRightOfSelectionRange(e.getRangeAt(0).getClientRects());    return this.button.style.position = "absolute", this.button.style.top = `${n}px`, this.button.style.left = `${t}px`, this  }  hide() {    return this.setDisplayStyle("none")  }  show() {    const e = window.getSelection(),      n = e?.toString()?.match(oe.SOURCE_LINK_REGEX)?.filter(ie);    return e && n && n.length ? (R.web_plugin_save_button_show({      scene: d.SELECT    }), this.setSourceLinks(n).setPosition(e).setDisplayStyle("block").appendToBody()) : this  }}

Copy handler reads the clipboard before matching

content.js
async copy() {  try {    if (Un()) return;    if (!(await Ye(a.OPTIONS_SETTING))?.[a.OPTIONS_SETTING][s.ANALYZE_CLIPBOARD_CONTENT]) return;    const e = await navigator.clipboard.readText();    if (Hn === e) return;    Hn = e;    const n = e.match(oe.SOURCE_LINK_REGEX);    if (!n) return;    Wn.show(n, {      scene: d.CLIPBOARD    })  } catch (e) {    console.log("Failed to read clipboard contents: ", e)  }}

Background script injects content.js into updated tabs

background.js
L = async (e, a, t) => {  try {    if (!a.status || t.url?.startsWith("chrome")) return;    await chrome.scripting.executeScript({      target: {        tabId: e      },      files: ["content.js", "ppa.min.js"]    })  } catch (e) {    console.log(e)  }}
Updated 30 September 2026ohpgjiplhigpfcioaeakjfjnkbgjpgbl