资讯动态

从XSS攻击原理到防御落地:跨站脚本漏洞的完整拆解与实践指南

发布时间:2026/10/20 21:54:42 来源:尧图企业网站定制
1. 一次真实场景的XSS攻击复盘留言区里的一行字把我朋友的公司挖穿了一半先说个我亲身参与过的案例。某创业团队的电商后台突然出现“大量账号异地登录”排查了一上午都没找到问题。最后我在他们商品详情页的“规格说明”字段里发现了一行不到三十个字符的代码。那段代码伪装成正常的商品文字用户只要打开商品页浏览器就会悄悄把本域 Cookie 发到另一个域名——标准的存储型 XSS。那家公司从客服账号到财务后台整整一个下午被对方“逛”了个遍原因就在于某个运营同学把商品说明里复制来的一段带标签的文本直接存进了数据库页面渲染时又没有做任何处理。这个案例我想表达的不是“创业团队安全意识差”而是想让你看清楚一件事XSS跨站脚本攻击不是实验室里才有的话题它就藏在你我天天都会写的搜索框、评论区、富文本编辑器还有 URL 参数里。绝大多数开发者见过 XSS 的漏洞报告但真正把它讲透、讲明白的人不多。这篇长文会从原理、分类、实战演示到防御方案把 XSS 整个生命周期完整拆给你看。不管你是刚入门的前端新人还是写了好几年接口的后端熟手又或是做安全测试的同行这篇内容能帮你把“XSS”三个字从一知半解变成真正吃透。我在这行做了十余年见过的漏洞报告里 XSS 出现频率常年排第一。它不是最“高级”的漏洞但它的花样绝对是最多的而且一旦和其它漏洞组合杀伤力远超想象。下面我按自己理解的深度从最基础的概念一路拆到防御落地方案全程说人话不拽术语。2. XSS 的三种常见形态一次给你说清2.1 存储型 XSSStored XSS最危险、最持久的“定时炸弹”存储型 XSS 的核心特征是恶意脚本被持久化存储在服务器端比如数据库每当用户访问包含此数据的页面时脚本都会被执行。这就不是“谁点谁中招”的问题了而是“谁看谁中招”——只要那个带毒内容不被人删掉它就能持续轰炸所有访客。它最常见的落脚点包括留言板、评论区、个人简介、商品详情、富文本编辑器内容。攻击者提交一段scriptalert(document.cookie)/script之类的代码如果服务器端不转义、前端渲染也不处理那么每个打开页面的用户都会中招。我复盘过最多的线上事故十有八九都出现在这种业务场景。2.2 反射型 XSSReflected XSS需要“诱骗点击”的临时性攻击反射型 XSS 的恶意代码不进入数据库而是藏在 URL 参数比如搜索关键词、跳转路径里由服务端拼接响应时“反射”回页面。攻击者不能直接攻击服务器而是需要设计一个带恶意参数的链接诱导用户点击。举个典型场景某网站的搜索框把用户输入的 keyword 原样回显URL 长这样https://example.com/search?keywordxxx攻击者构造一个链接https://example.com/search?keywordscriptalert(document.cookie)/script受害者点开这个链接脚本就在受害者浏览器里执行了。这种攻击的“杀伤半径”取决于攻击者能不能骗到人点击链接通常结合钓鱼场景使用。想写链接就失误典型的“一念之差”。实操提醒反射型XSS属于“点了才有事”但你别因为这样就觉得它危害小。如果目标网站上有一个支持 URL 跳转的接口再加上返回页的反射参数在钓鱼场景下配合社工话术成功率相当可观。2.3 DOM 型 XSSDOM-based XSS全程不经过后端的“内鬼”DOM 型 XSS 最容易被新人忽略因为它的整个攻击链只发生在浏览器端恶意代码直接对页面 DOM 结构进行操作服务器完全不参与payload 甚至不需要发到后端。问题根源出在前端 JS 代码使用了document.write、innerHTML、location等接口把不可信数据当成了 HTML 来渲染。典型示例const name new URLSearchParams(window.location.search).get(name); document.getElementById(welcome).innerHTML 欢迎您 name;正常访问时页面显示“欢迎您某某”。但如果 URL 是?nameimg srcx onerroralert(document.cookie)浏览器解析到 innerHTML 时会把这段字符串当成 HTML 渲染onerror 事件触发XSS 弹出。整个过程中服务端没有收到任何多余数据所以服务端日志检查、WAF 过滤全都不起作用——这也是它最隐蔽的原因。我把三种类型放一张对照表里方便你记忆类型数据存储位置payload 传递方式触发时机危险程度存储型数据库/服务器正常提交数据任意用户打开页面即触发极危险影响面广反射型不存储URL 参数、表单提交用户点击恶意链接时触发较危险需要欺骗用户DOM 型浏览器 DOMURL 参数/Hash前端 JS 运行时触发隐蔽绕过难度大3. 从输入到执行一个 XSS payload 是怎么“破笼而出”的3.1 “文本”和“代码”之间只有一层编码的窗户纸XSS 之所以防不胜防最根本的原因是浏览器本身就有“HTML 解析器”和“JS 引擎”双功能。当服务器返回一个字符串浏览器必须判断它到底是纯文本、HTML标签、还是JS代码。这个判断不是靠“长得像什么”决定的而是看字符串当前位置的上下文。开发者的本意是把用户输入当成“文本”展示。可一旦用户输入里包含了尖括号、引号、script标签等特殊字符浏览器的 HTML 解析器就会把它们当作“HTML 标签结构”去解析从而“破笼而出”。你可以把浏览器想象成一台自动门禁开发者希望数据走“文本通道”但攻击者输入的特殊字符能骗过门禁让数据进入“代码通道”。3.2 编码链路一个 payload 在到达浏览器前经历了哪些变形真实的攻击 payload 往往不会像scriptalert(1)/script这么赤裸裸地出现在 URL 里。因为现代系统多少有些过滤攻击者会做多重编码。举一个实战中常见的编码链路攻击者最初的 payloadimg srcx onerroralert(document.cookie)为了骗过服务端过滤如果服务端过滤了img关键字攻击者会把 URL 参数进行百分号编码%3Cimg%20src%3D%22x%22%20onerror%3D%22alert(document.cookie)%22%3E服务端在拼接进 HTML 响应时如果用了urldecode()或者前端decodeURIComponent()浏览器收到响应后会先解码 URL 再解析 HTML此时尖括号重新出现img 标签生效脚本执行。还有更绕的做法比如在 JS 字符串上下文中利用 HTML 实体编码嵌套a href# onclickvar msg#x3c;script#x3e;alert(1)#x3c;/script#x3e;;这个例子中#x3c;是的十六进制 HTML 实体浏览器在解析文本时会先解码成如果后续代码把这个字符串当成 HTML 插入XSS 又成立了。关键点在于编码链可能有多层每一层解码后都会“还原”一部分只要最终能够还原成可执行的 HTML/JS攻击就成功。3.3 常见的“破笼”技巧到底在打破什么我在这里换个角度给你整理常见的载荷变形手法重点不是让你拿去攻击谁而是为了让你理解“为什么黑名单过滤必然失效”大小写绕过ScRiPt面对不区分大小写的解析器时与script效果相同。标签闭合如果页面结构是input value用户输入攻击者输入scriptalert(1)/script就能先闭合 value 属性再注入新标签。无 script 标签的载荷img srcx onerroralert(1)、svg/onloadalert(1)、body onfocusalert(1) autofocus这些全都不依赖script标签。重复参数覆盖某些框架对重复参数的处理不一致攻击者利用它绕过取参时的第一道过滤。JS 伪协议a hrefjavascript:alert(1)点我/a用户点击链接时执行 JS。这些技巧说明一件事过滤“script”关键字是堵不住所有攻击路径的因为可执行的 HTML 标签实在太多了。你无论写出多长的黑名单总有新思路能把浏览器带到执行代码这一步。所以防御的方向要变这个我们后面在防御章节详细说。4. 自己动手搭一个 XSS 靶场五分钟看透整个攻击链路光看原理不够我建议你亲手搭一个极简靶场。我下面给出一个不用装任何复杂软件、本地就能跑的实验环境。这个实验的目标不在于“教你攻击”而在于让你直观看到数据从提交、存储、回显到执行的全过程。4.1 靶场架构一个最简留言板我用 Node.js 写一个极简服务只有一个页面一个接口数据存在内存数组里仅供本地实验。!-- 前端页面index.html -- !DOCTYPE html html headmeta charsetutf-8titleXSS靶场/title/head body h2留言板/h2 form idmsgForm input typetext idmsg placeholder说点什么... stylewidth:300px; button typesubmit提交/button /form ul idlist/ul script // 加载留言 fetch(/messages).then(r r.json()).then(data { const ul document.getElementById(list); data.forEach(item { // 这里故意使用 innerHTML 渲染作为漏洞演示 ul.innerHTML li item.content /li; }); }); // 提交留言 document.getElementById(msgForm).addEventListener(submit, async (e) { e.preventDefault(); const content document.getElementById(msg).value; await fetch(/messages, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({content}) }); location.reload(); }); /script /body /html// 服务端server.js需先 npm install express const express require(express); const app express(); app.use(express.json()); app.use(express.static(__dirname)); let messages []; app.get(/messages, (req, res) { res.json(messages); }); app.post(/messages, (req, res) { // 没有任何过滤直接存 messages.push({ id: messages.length 1, content: req.body.content }); res.json({ ok: true }); }); app.listen(3000, () console.log(靶场已启动: http://localhost:3000));运行node server.js打开http://localhost:3000就能开始实验了。4.2 三个实验把三种触发方式各看一遍实验一直接注入 script。在输入框里填scriptalert(xss)/script提交后刷新你会看到弹窗。原因是服务端原样保存前端 innerHTML 把它渲染成了脚本标签。实验二用 img onerror 触发。输入img srcnope onerroralert(document.cookie)。这里就算没有 script 也照样弹 Cookie因为图片加载失败触发了 error 事件。实验三体验漏洞页面的“真实受害场景”。用无痕模式打开一个新的浏览器窗口访问同一个页面。因为留言是共享的新窗口同样会弹窗。这就是存储型 XSS 的传播方式——所有访问者都会中招。你可以打开 DevTools 的 Network 面板看到完整的请求/响应过程注意Accept: text/html那段响应体里你输入的内容原封不动躺在里面。注意做实验请只在自己本地跑不要拿这套逻辑去测别人的站点。没有授权对第三方系统做安全测试属于违法操作这条红线千万别碰。写代码和学安全的所有前提都是“用于授权范围内的建设”而不是“破坏”。4.3 在浏览器控制台里“看穿”执行过程当弹窗出现时你可以在 DevTools 的 Elements 面板里看到被渲染后的 DOM 结构。你会发现刚才输入的script已经被当作一个真正的 script 节点解析而img onerror也已经插入到 DOM 树中。这个观察非常关键你输入的是字符串浏览器看到的却是可执行节点。理解这一点后面防御方案的设计逻辑就很自然了——我们要做的不是“洗掉脏字符”而是让用户的输入永远以“文本节点”的形式出现在 DOM 中。5. XSS 能做什么为什么说它是“弹药库”5.1 偷 Cookie、冒充身份、会话劫持XSS 最有名的利用就是偷 Cookie。只要页面里能执行 JS攻击者就能通过document.cookie拿到当前域下的 CookieHttpOnly 标记的除外然后构造一个带 Cookie 的请求重放出来从而“扮演”受害者的身份。对业务系统的危害首先是账号接管其次是越权操作。但我必须强调两点实操认知HttpOnly 能拦住一部分拦不住所有。因为攻击者完全可以在当前页面里直接代发请求不需要把 Cookie 偷走也能操作受害者账户。真正值钱的不是 Cookie而是“会话上下文”。攻击者更关心受害者在当前系统里的身份、权限和数据。所以我经常说XSS 是“弹药库”因为它能帮你绕过前端所有防线直接以受害者的身份与后端交互。5.2 页面篡改与钓鱼把官网变成诈骗页面有执行 JS 的能力攻击者就能改 DOM把原本正常的页面换成钓鱼页面。比如注入一个假登录框覆盖在原页面上用户输入账号密码后脚本把数据偷偷发送到攻击者服务器。更隐蔽的做法是改掉页面上所有的真实链接让用户点击后跳转到仿冒页面。5.3 键盘记录与敏感信息窃取通过 JS 事件监听keydown、input、change记录用户在表单里的每一次按键。用户的搜索词、密码输入、聊天内容都可能被实时上传。这种利用方式通常还需要配合“动态通信”通道把自己的结果回传出去。在我参与过的应急响应案例中就有人靠一段窃取输入内容的脚本在几天内收集了上百组账号口令——直到受害者发现自己的账户有异地登录行为才察觉。你需要清醒地认识到以上所说的每一种利用本质上都是“可以执行 JS”这个能力带来的。XSS 是漏洞的起点由它展开的攻击路径五花八门。所以修复 XSS 的意义在于不让攻击者拿到“可以执行脚本”这个令牌。6. 防御不靠过滤黑名单从源头堵住脚本注入6.1 核心心法输出编码而不是输入过滤很多人一听说防 XSS 就说“过滤用户输入、把尖括号替换掉”。这个思路不能说完全错但它方向偏了。原因我们前面已经验证过黑名单永远有考虑不到的标签和编码组合。正确的心法是对用户输入的数据不做刻意的“清洗”而是在它要输出到 HTML/JS/CSS/URL 的每一个上下文里做对应的输出编码。上下文不同编码方式完全不同输出到HTML 普通文本节点转义、、、、五个字符。输出到HTML 属性值转义引号和尖括号防止冲出属性边界。输出到JS 字符串转义反斜杠、引号、换行等防止闭合字符串。输出到URL 上下文做 URL 编码。以 PHP 的htmlspecialchars为例?php echo htmlspecialchars($userInput, ENT_QUOTES, UTF-8); ?它会将输入中的、、、、转成无害实体浏览器在 HTML 上下文中会把它们当作纯文本渲染而不是标签结构。这就是“输出编码”的标准姿势。前端框架的默认行为其实已经帮你做了这件事比如 React 和 Vue 在模板绑定文本时{ { } }中间的值默认会对字符串进行转义但如果用了v-html、dangerouslySetInnerHTML就等于跳过了默认保护得你自己对数据消毒或者确保来源可信。6.2 设置信任边界让富文本编辑器的输入走“白名单”有些业务场景确实需要让用户输入图片、链接、加粗等格式化内容比如文章编辑不能一刀切禁止 HTML。这种场景的正确姿势是“白名单过滤”只允许白名单中的标签p, strong, em, a, img, ul, li, code, blockquote等。每个标签只允许白名单中的属性比如a只允许href、titleimg只允许src、alt并且src的协议还得限制为http、https、data:image/等。不允许任何事件属性onclick、onerror、onload和style属性style也能玩出花样光background:url(javascript:...)就够喝一壶。以上逻辑要用成熟库比如富文本编辑器自带的 sanitize 功能或者专门的 sanitize 库不要自己写正则应付。6.3 上利器CSP内容安全策略把执行权限关进笼子里CSP 是一套由服务器通过响应头下发给浏览器的规则它告诉浏览器“哪些来源的脚本可以执行”默认情况下禁止所有内联脚本。你头一次看到 CSP 可能会觉得配置很啰嗦但它对 XSS 的拦截能力是“结构性”的不是靠过滤字符串。一个典型的 CSP 响应头Content-Security-Policy: default-src self; script-src self; object-src none; base-uri none;意思是所有内容只允许来自同源脚本只允许同源加载禁用 object/embedbase标签也不许。这个策略下即使攻击者注入了一个script srchttp://evil.com/x.js浏览器也会直接拒绝加载并 Console 里报 CSP violation 错误。如果业务确实需要加载内联脚本现在很多构建工具会把 JS 打包成外链文件内联量其实很少可以用nonce模式Content-Security-Policy: script-src self nonce-随机串;服务端每次响应时给合法脚本一个随机 nonce只有带这个 nonce 的script才允许执行。攻击者注入的脚本拿不到这个 nonce直接作废。有个细节值得注意CSP 不是万能药。如果你的页面里某个第三方回调接口本身不可控CSP 也可能被你自己的合法脚本“带跑偏”。所以合理的策略是纵深防御输出编码是第一道防线CSP 是兜底的第二道防线两者都做而不是选一个。6.4 Cookie 相关的三个安全属性别漏了封堵执行脚本能力的同时我们要把“即使执行了损失也要降一点”的兜底措施做全HttpOnly这个属性设置后document.cookie拿不到该 Cookie。对主要用于会话认证的 Cookie比如 sessionid强烈建议开启。Secure仅允许 HTTPS 下传输。SameSite控制跨站请求是否携带 CookieSameSiteLax或Strict能有效降低跨站请求伪造CSRF与部分跨站信息泄露风险。这三个属性加在 Set-Cookie 响应头上例如Set-Cookie: sessionidabc123; HttpOnly; Secure; SameSiteLax7. 排查 XSS 漏洞的实用思路与工具7.1 手工排查三步法如果你在审一个既有系统前端和后端都归你管建议按这个顺序过先找拼接点。到代码里搜索innerHTML、document.write、v-html、dangerouslySetInnerHTML、eval、new Function这些危险调用看它们的数据流是从哪里来的——有这些调用的地方就是 DOM XSS 的高危区域。再找后端回显点。到服务端模板文件里搜%、?、{{ }}这类模板变量输出看有没有变量没有被转义就拼进了 HTML。最后实测。在页面上所有可输入的位置搜索、评论、昵称、URL 参数都填入测试载荷。基础测试用scriptalert(document.domain)/script就行若被过滤再尝试大小写和 img/onerror 载荷看哪一层能绕过。7.2 用浏览器原生能力做快速探测不必一上来就搬自动化工具。先用 Chrome 的 DevTools 就能做很多事在 Console 面板手动执行document.cookie观察当前页面 Cookie 可达性。在 Elements 面板里观察输入内容是否被渲染为 HTML 节点。使用“Network”面板看请求和响应详情确认响应体里是否有未转义回显。如果开发环境下有构建工具还可以利用浏览器的“覆盖本地文件”Overrides功能临时修改响应内容来验证“如果这里注入 XSS能不能执行”方便很多。7.3 自动化扫描的正确打开方式市面上有不少开源扫描器能在目标站点上自动提交 payload 并根据响应特征判定漏洞。我把使用原则总结为三条你必须有授权或者目标是你自己的系统。扫描结果只是“疑似”必须人工复现确认否则误报能淹死你。扫描前先把业务摸清楚别拿爆破 payload 打到人家下单、支付这类流程上。我的经验是自动化工具能帮你快速扩大覆盖面但最终写漏洞报告、提修复方案靠的还是你对业务逻辑和上下文编码的理解。8. 我踩过的坑XSS 修复中最容易犯的错最后分享几条十几年里反复遇到的、极容易踩的坑。第一个坑只在用户输入时过滤。有人做了一套“通用过滤中间件”所有入参都替换尖括号结果别的地方拼接参数到 URL 时又解了一遍码防御宣告失效。过滤发生在输入阶段你根本不知道这个值将来会被用在哪个上下文与其“猜”不如在所有输出点统一做上下文编码。修复 XSS 的第一原则就是在输出点动手别在输入点猜。第二个坑只过滤script标签。屡见不鲜。有人把script字符串换掉就宣布“已修复”。结果我用一个img srcx onerroralert(1)三秒内给它打回原形。做安全不是“过家家”要在理解攻击面的基础上去防护而不是写一道能过“测试用例”就沾沾自喜。第三个坑忽略 DOM XSS。很多团队的代码评审重点全在后端接口和模板输出上完全忘了前端用v-html、innerHTML拼接 URL 参数的地方。前两年一个被广泛讨论的漏洞案例里某产品就是因为在 URL 参数解析后直接拼到了 HTML 上所有用户数据泄露原因就是 DOM XSS 没人审。第四个坑以为加了 CSP 就一劳永逸。如果页面里本身就存在不安全的拼接点攻击者注入的 payload 即使不能加载外部脚本也可能利用你页面上已有的合法脚本、或者通过meta标签等方式绕过 CSP 的限制做点“小动作”。CSP 是降低风险面的好工具不是免死金牌。给你留个自测问题如果你拿到一个页面搜索框把参数原样回显到h1标签里你应该怎么修我建议你先想清楚这是 HTML 文本上下文还是属性上下文然后再决定转义哪些字符——想清楚这个说明你对 XSS 的理解已经超过大多数人了。我在实际项目里见过太多“修了个寂寞”的漏洞修复也见过一条严谨的编码方案堵住整条攻击链。写这篇长文的初衷就是希望你遇到 XSS 时心里有底知道它怎么来、怎么防、怎么查而不是看到一堆script就发懵。如果你照着上面的靶场流程亲手操作一遍相信你对“输出编码”这四个字的理解会比以前深一个层次。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑