Is 哔哩哔哩下载助手 safe?
哔哩哔哩下载助手 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.…
Who publishes ittomxuxu - 9 other listings from the same operator, 4 of them carrying a finding
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.
Same store account
9 other listings published from this account, 94k+ users between them. 4 of them carry a finding.
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
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.
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.
- Proxy destinationagency.pictureknow.com:443
Shows the outside host that receives the selected update traffic instead of a direct connection.
- Google update hostclients2.google.com
Shows which update service your browser was trying to reach when the PAC rule applies.
- Update asset hostclients2.googleusercontent.com
Shows that update-related asset requests are also selected by the proxy rule.
- Update request pathhttps://clients2.google.com/service/update2/crx
Shows the Chrome update endpoint associated with this extension's manifest.
The direct request fails in the proxy path; no request body is recorded for this claim.
The proxy host and targeted Google hostnames are stored as AES-CBC strings and decrypted before the PAC rule is built.
{ "protocol": "https", "server": "agency.pictureknow.com", "extension_update": "clients2.google.com", "extension_update_address": "clients2.googleusercontent.com"}Code path that builds and applies the PAC proxy rule
Encrypted constants and AES helper
js/background.jsvar 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() })) }}- 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.
Recomputes the AES key from the bundled seed and decrypts the proxy constants so you can verify the PAC inputs.
- Node.js 18+
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));- 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
You open or use the extension popup.
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.
- Extension IDdjinnjdnedmcilpnboifdhfgbbhgieee
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 stateenabled: true
This shows whether an installed add-on is active or turned off.
- Install type and update URLinstallType: 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.
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'
{ "extensions_weight": [ { "id": "djinnjdnedmcilpnboifdhfgbbhgieee", "weight": 2 } ]}The popup reads and displays the installed-extension list
Popup initialization, inventory read, and ranking storage
deobfuscated/js/popup.jskey: "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.jsfunction 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.jskey: "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 manifest grants management access to the popup bundle
"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
You open a Bilibili video or bangumi page.
e.g. www.bilibili.com/video/BV1FAuJ6LEDy
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.
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 status check that gates the download feature
video_bilibili_content.js — getStatus() / requestStatus() (pretty-printed)
js/video_bilibili_content.jskey: "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.jskey: "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) })) }))}}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.
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'
{ "time": 1786873086053, "status": true}- 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