Is 哔哩哔哩下载助手 safe?

High risk

哔哩哔哩下载助手 is high risk. When the extension can't reach clients2.google.com, it installs a Chrome PAC proxy rule for that host and clients2.googleusercontent.com, routes those requests via agency.pictureknow.com over HTTPS, checks updates, then clears the proxy.…

tomxuxuv1.5.7Chrome Web Store
78Risk
Who publishes it

tomxuxu - 9 other listings from the same operator, 4 of them carrying a finding

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

Publisher
tomxuxu

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

Publishers can request a review.

Findings

Chrome Update Requests Routed Through PictureKnow Proxy

When the extension can't reach clients2.google.com, it installs a Chrome PAC proxy rule for that host and clients2.googleusercontent.com, routes those requests via agency.pictureknow.com over HTTPS, checks updates, then clears the proxy.

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

The extension sees a newer PictureKnow version and then cannot reach Google's update host directly.

That failed direct request is the condition that leads to the proxy path.

The extension did this

It installs a PAC proxy rule that sends selected Google update-host traffic through agency.pictureknow.com.

The code then asks Chrome to check for an extension update and schedules the proxy setting to be cleared.

Fields selected by the proxy rule
  • Proxy destination
    agency.pictureknow.com:443

    Shows the outside host that receives the selected update traffic instead of a direct connection.

  • Google update host
    clients2.google.com

    Shows which update service your browser was trying to reach when the PAC rule applies.

  • Update asset host
    clients2.googleusercontent.com

    Shows that update-related asset requests are also selected by the proxy rule.

  • Update request path
    https://clients2.google.com/service/update2/crx

    Shows the Chrome update endpoint associated with this extension's manifest.

Captured request
GEThttps://clients2.google.com/

The direct request fails in the proxy path; no request body is recorded for this claim.

Why you can't catch this in DevTools

The proxy host and targeted Google hostnames are stored as AES-CBC strings and decrypted before the PAC rule is built.

Decoded value
{  "protocol": "https",  "server": "agency.pictureknow.com",  "extension_update": "clients2.google.com",  "extension_update_address": "clients2.googleusercontent.com"}
The code that does this

Code path that builds and applies the PAC proxy rule

Readable version

Encrypted constants and AES helper

js/background.js
var l = r(1354),  h = r.n(l),  p = ["1=", "_", "23", "4a5", "6e7", "qW"],  v = p[0] + p[1] + p[2] + p[3] + p[4] + p[5],  d = h().MD5(v),  y = "q23-o6+21z34Q6M8";var g = {    encrypte: function(t) {      var e = function(t, e, r) {        return e = h().enc.Utf8.parse(e), r = h().enc.Utf8.parse(r), h().AES.encrypt(t, e, {          iv: r,          mode: h().mode.CBC,          padding: h().pad.Pkcs7        }).toString()      }(t, d, y);      return h().enc.Utf8.parse(e), e    },    decrypte: function(t) {      return function(t, e, r) {        return e = h().enc.Utf8.parse(e), r = h().enc.Utf8.parse(r), h().AES.decrypt(t, e, {          iv: r,          mode: h().mode.CBC,          padding: h().pad.Pkcs7        }).toString(h().enc.Utf8)      }(t, d, y)    }  },  _ = {    host: "https://api.pictureknow.com/",    icon: "chrome://favicon/size/32@2x/",    version: "1.5.7",    server: "3tJZbGljQMTh4BiwNY6RiP0p8zrtpuQog4e7vsoKjmA=",    protocol: "LBgCRNF/XO5qKkMQjaUkug==",    extension_search: "ulFqfrH+gDUxdnLE0RYmwA==",    extension_update: "Sn3PKraH7Z6IZjWL4PXJCTzFsmAUJzFH/gYfjLZINqU=",    extension_update_address: "MaOv/OAu8yLhX7CS0bSH+7blEGf331Zf8e1O7CbHbEY="  };

Update fallback, proxy installation, and cleanup

js/background.js
{  key: "update",  value: function() {    var t = this;    fetch("https://clients2.google.com/").then((function(t) {})).catch((function(e) {      chrome.proxy.settings ? chrome.proxy.settings.get({}, (function(e) {        var r, n;        n = e.value, "controlled_by_this_extension" === (r = e.levelOfControl) ? t.forceCheckUpdate() : "controllable_by_this_extension" === r && "system" === n.mode && t.setProxy((function() {          t.forceCheckUpdate()        }))      })) : t.clearProxy()    }))  }}, {  key: "forceCheckUpdate",  value: function() {    var t = this;    chrome.runtime.requestUpdateCheck((function(e, r) {      t.clearProxy()    })), w.createAlarm("clear-proxy", 1), w.listen("clear-proxy", (function() {      t.clearProxy()    }))  }}, {  key: "clearProxy",  value: function() {    chrome.proxy.settings && chrome.proxy.settings.get({}, (function(t) {      "controlled_by_this_extension" === t.levelOfControl && chrome.proxy.settings.clear({})    }))  }}, {  key: "setProxy",  value: function(t) {    var e = g.decrypte(_.protocol),      r = g.decrypte(_.server),      n = (g.decrypte(_.extension_search), {        mode: "pac_script",        pacScript: {          data: 'let p = "' + e + '";let s = "' + r + '";function FindProxyForURL(u, h) {if (h == "' + g.decrypte(_.extension_update) + '") {return p + " " + s + "; DIRECT";}if (h == "' + g.decrypte(_.extension_update_address) + '") {return p + " " + s + "; DIRECT";}return "DIRECT";}'        }      });    chrome.proxy.settings.set({      value: n,      scope: "regular"    }, (function() {      "function" == typeof t && t()    }))  }}
Hosts involved in the update path
    • agency.pictureknow.com

    HTTPS proxy host produced by decrypting the bundled server constant; selected Google update-host traffic is routed there by the PAC script.

    • api.pictureknow.com

    PictureKnow version endpoint checked before the proxy fallback path can run.

    • clients2.google.com

    Google update host used for the direct reachability probe and selected by the PAC rule.

    • clients2.googleusercontent.com

    Google update asset hostname selected by the PAC rule.

Reproduce it yourself

Recomputes the AES key from the bundled seed and decrypts the proxy constants so you can verify the PAC inputs.

Requires
  • Node.js 18+
decode-pictureknow-proxy.js · js
const crypto = require('crypto');const seedParts = ['1=', '_', '23', '4a5', '6e7', 'qW'];const seed = seedParts.join('');const keyHexText = crypto.createHash('md5').update(seed, 'utf8').digest('hex');const key = Buffer.from(keyHexText, 'utf8');const iv = Buffer.from('q23-o6+21z34Q6M8', 'utf8');const constants = {  server: '3tJZbGljQMTh4BiwNY6RiP0p8zrtpuQog4e7vsoKjmA=',  protocol: 'LBgCRNF/XO5qKkMQjaUkug==',  extension_search: 'ulFqfrH+gDUxdnLE0RYmwA==',  extension_update: 'Sn3PKraH7Z6IZjWL4PXJCTzFsmAUJzFH/gYfjLZINqU=',  extension_update_address: 'MaOv/OAu8yLhX7CS0bSH+7blEGf331Zf8e1O7CbHbEY=',};function decrypt(value) {  const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);  decipher.setAutoPadding(true);  return Buffer.concat([decipher.update(Buffer.from(value, 'base64')), decipher.final()]).toString('utf8');}const decoded = Object.fromEntries(  Object.entries(constants).map(([name, value]) => [name, decrypt(value)]),);const pac = `let p = "${decoded.protocol}";let s = "${decoded.server}";function FindProxyForURL(u, h) {if (h == "${decoded.extension_update}") {return p + " " + s + "; DIRECT";}if (h == "${decoded.extension_update_address}") {return p + " " + s + "; DIRECT";}return "DIRECT";}`;console.log(JSON.stringify({ seed, key_md5_hex: keyHexText, iv: iv.toString('utf8'), decoded, pac }, null, 2));
How to run it
  1. 1node decode-pictureknow-proxy.js

Popup inventories installed extensions

Opening the popup lets the extension list installed extensions via Chrome management access, showing names, versions, icons, and controls, saving IDs with ranking weights in synced storage.

No network request with this data was observed.

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

You open or use the extension popup.

The extension did this

The popup reads your installed-extension inventory and stores ranking choices for extension IDs in synced storage.

Dynamic verification confirmed the management permission and the popup code path; no request carrying the list was observed.

Installed-extension data handled by the popup
  • Extension ID
    djinnjdnedmcilpnboifdhfgbbhgieee

    This identifies a specific add-on installed in your browser and can reveal what tools or services you use.

  • Extension name
    哔哩哔哩下载助手

    This shows the human-readable name of an installed add-on in your browser.

  • Enabled state
    enabled: true

    This shows whether an installed add-on is active or turned off.

  • Install type and update URL
    installType: normal, updateUrl: https://clients2.google.com/service/update2/crx

    This distinguishes development, local, app, and store-updated extensions.

  • Ranking weight
    {"id":"djinnjdnedmcilpnboifdhfgbbhgieee","weight":2}

    This records which extension IDs you pinned to the top of the popup list and syncs that preference through Chrome storage.

What's stored on your device

Chrome synced storage can carry your ranked extension IDs between browser profiles signed into the same Chrome account.

Location
chrome.storage.sync key 'extensions_weight'
Contents
{  "extensions_weight": [    {      "id": "djinnjdnedmcilpnboifdhfgbbhgieee",      "weight": 2    }  ]}
The code that does this

The popup reads and displays the installed-extension list

Readable version

Popup initialization, inventory read, and ranking storage

deobfuscated/js/popup.js
key: "UNSAFE_componentWillMount",value: function() {  var e = this;  c.get().then((function(t) {    t.extensions_weight ? e.setState({      items_weight: t.extensions_weight    }, e.get_extension()) : e.get_extension()  }))}}, {key: "get_extension",value: function() {  var e, t = this;  e = function(e) {    var n = [],      r = [];    e.forEach((function(e) {      t.state.items_weight.forEach((function(t) {        t.id === e.id && (e.weight = t.weight)      })), chrome.i18n.getMessage("@@extension_id") !== e.id && (e.isApp ? e.form = "APP" : "development" === e.installType ? e.form = "DEV" : e.updateUrl ? e.form = "" : e.form = "L", e.weight || (e.weight = 1), e.icons && e.icons.length > 0 && (e.enabled ? n.push(e) : r.push(e)))    })), t.setState({      enable_data: n.sort((function(e, t) {        return t.weight - e.weight      })),      disable_data: r.sort((function(e, t) {        return t.weight - e.weight      }))    })  }, chrome.management.getAll((function(t) {    e(t)  }))}}, {key: "handleTop",value: function(e) {  var t = this.state.items_weight,    n = 1;  t.length && (t.sort((function(e, t) {    return t.weight - e.weight  })), n = t[0].weight), t.push({    id: e,    weight: n + 1  }), this.setState({    items_weight: t  }), this.get_extension(), c.get().then((function(e) {    e.extensions_weight = t, c.save(e)  }))}}

Popup controls for enabling and uninstalling other extensions

deobfuscated/js/popup.js
function r(e, t, n) {  chrome.management.setEnabled(e, t, (function(e) {    n(e)  }))}key: "handleUninstall",value: function(e) {  var t = this;  ! function(e, t) {    chrome.management.uninstall(e, {      showConfirmDialog: !0    }, (function(e) {      t(e)    }))  }(e, (function(n) {    c.get().then((function(n) {      if (n.extensions_weight) {        var r = [];        n.extensions_weight.forEach((function(t) {          e !== t.id && r.push(t)        })), t.setState({          items_weight: r        }), n.extensions_weight = r, c.save(n)      }    })), t.get_extension()  }))}

Popup rendering of extension names, versions, icons, and actions

deobfuscated/js/popup.js
key: "get_extensions",value: function() {  var t = this,    n = this.state.enable_data.map((function(n, r) {      return e.createElement("div", {        key: n.id,        className: t.state.searchKeyword && !n.sign ? "item item-hidden" : "item"      }, e.createElement("img", {        onClick: t.handleEnabled.bind(t, n.id, !1),        src: n.icons[n.icons.length - 1].url      }), e.createElement("button", {        onClick: t.handleOptions.bind(t, n, r),        className: "options"      }), n.form.length > 0 && e.createElement("i", null, n.form))    })),    r = this.state.disable_data.map((function(n, r) {      return e.createElement("div", {        key: n.id,        className: t.state.searchKeyword && n.sign ? "item item-show" : "item"      }, e.createElement("img", {        onClick: t.handleEnabled.bind(t, n.id, !0),        src: n.icons[n.icons.length - 1].url      }), e.createElement("button", {        onClick: t.handleOptions.bind(t, n, r),        className: "options"      }), n.form.length > 0 && e.createElement("i", null, n.form))    }));  return e.createElement("div", {    className: "content"  }, e.createElement("div", {    className: "enable"  }, n), e.createElement("div", {    className: "disable"  }, r), e.createElement("div", {    className: "menu",    style: this.state.style  }, e.createElement("div", {    className: "name"  }, this.state.item.name, e.createElement("span", {    className: "version"  }, " ", this.state.item.version)), e.createElement("ul", null, e.createElement("li", {    onClick: this.handleUninstall.bind(this, this.state.item.id)  }, "卸载"), this.state.item.isApp ? e.createElement("li", {    className: this.state.item.appLaunchUrl ? "" : "off",    onClick: this.handleLink.bind(this, this.state.item.appLaunchUrl)  }, "打开") : e.createElement("li", {    className: this.state.item.enabled && this.state.item.optionsUrl ? "" : "off",    onClick: this.handleLink.bind(this, this.state.item.optionsUrl, this.state.item.enabled)  }, "选项"), e.createElement("li", {    className: this.state.item.homepageUrl ? "" : "off",    onClick: this.handleLink.bind(this, this.state.item.homepageUrl)  }, "主页"), e.createElement("li", {    onClick: this.handleTop.bind(this, this.state.item.id)  }, "置顶"))))}
The code that does this

The manifest grants management access to the popup bundle

Readable version
"permissions": [  "alarms",  "proxy",  "management",  "tabs",  "declarativeNetRequest",  "storage"]

Remote Server Gates the Bilibili Download Feature

The extension asks api.pictureknow.com whether to show its download button, at most once per 24h.

We saw a GET to api.pictureknow.com/api/v1/page/info on a Bilibili visit; no personal data is sent, but a remote server gates the feature.

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

You open a Bilibili video or bangumi page.

e.g. www.bilibili.com/video/BV1FAuJ6LEDy

The extension did this

The extension's background service worker asks a remote server whether the download feature should be turned on, before it shows any download controls on the page.

The check is skipped only when a prior answer is already cached and less than 24 hours old.

Captured request
GEThttps://api.pictureknow.com/api/v1/page/info?id=2f8405eb077a4d17b127277996389071

200 OK, JSON body including valid:1 and name:'下载B站视频' (Chinese for "Download Bilibili Video"), captured during dynamic analysis while visiting a live www.bilibili.com/video/... page.

The code that does this

The status check that gates the download feature

Readable version

video_bilibili_content.js — getStatus() / requestStatus() (pretty-printed)

js/video_bilibili_content.js
key: "getStatus",value: function() {  var e, t = this;  (e = "video_bilibili_status", new Promise((function(t) {    chrome.storage.sync.get(e, (function(n) {      t(n[e])    }))  }))).then((function(e) {    e ? (new Date).getTime() > parseInt(e.time) + 864e5 ? t.requestStatus() : t.setState({      status: e.status    }) : t.requestStatus()  }))}}, {key: "requestStatus",value: function() {  var e = this,    t = new Date,    n = gr + "api/v1/page/info?id=2f8405eb077a4d17b127277996389071";  dr.sendMessage(new ur("request", {    url: n  })).then((function(n) {    var r, o, i = !1;    n.valid && (i = !0), r = "video_bilibili_status", o = {      time: t.getTime(),      status: i    }, new Promise((function(e) {      chrome.storage.sync.set(hr({}, r, o), (function() {        e(o)      }))    })), e.setState({      status: i    })  }))}}

background.js — the 'request' handler that proxies the fetch (pretty-printed)

js/background.js
key: "initMessageClient",value: function() {  u.listen("request", (function(t, e, r) {    fetch(t.params.url).then((function(t) {      return t.json()    })).then((function(t) {      r(t)    }))  }))}}
When this fires

Every 1 day

The status check re-runs at most once every 24 hours per browser profile; page visits inside that window reuse the cached answer without contacting the server again.

What's stored on your device

The cached answer to whether the download feature is on, refreshed daily. Chrome syncs this to every device on the same profile.

Location
chrome.storage.sync, key 'video_bilibili_status'
Contents
{  "time": 1786873086053,  "status": true}
Host controlling the feature toggle
    • api.pictureknow.com

    Returns the flag gating the Bilibili download feature. Shares its domain with the extension's own conversion tool and help site, i.e. developer-operated infrastructure.

What it can do

Permissions this extension asks for, as declared in version 1.5.7. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.

  • Read and change your data on every site you visit

    <all_urls>

  • Schedule its own background tasks

    alarms

  • Route all of your browsing through a server of its choosing

    proxy

  • See, disable and uninstall your other extensions, including your security ones

    management

  • See the address and title of every tab you have open

    tabs

  • Block and redirect the requests your browser makes

    declarativeNetRequest

  • Store data in your browser

    storage

Updated 30 September 2026djinnjdnedmcilpnboifdhfgbbhgieee