一句话解释
CSRF(Cross-Site Request Forgery,跨站请求伪造),是一种网络攻击方式。
攻击者诱使用户 在已认证的网站 上,发送预期外的请求。
本质是利用了 cookie 自动携带的特性。
可针对 cookie 作为认证信息,且没进行相关防御的网站。
流程图
代码实现
银行网站 https://bank.com
注意,https 协议与 http 协议一样,是没有办法防范 csrf 的。
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=, initial-scale=1.0"
>
<title>简易的银行系统</title>
</head>
<style>
.hidden {
display: none;
}
</style>
<body>
<!-- 登录界面 -->
<div id="userLogin">
<h2>用户登录</h2>
<div class="login-form">
<input
type="text"
placeholder="用户名"
value="chp"
id="userName"
>
<input
type="password"
id="userPsd"
>
</div>
<button onclick="handleLogin()">登录</button>
</div>
<!-- 登录成功后的页面 -->
<div
id="userContent"
class="hidden"
>
<h2>用户登录成功</h2>
<p>欢迎回来,demochUser!</p>
<p>您的账户安全级别为:【几乎没有】</p>
</div>
<button onclick="handleLogout()">登出</button>
<input
id="money"
type="number"
value="1000"
placeholder="请输入您要转账的金额"
/>
<button onclick="transfer()">转账</button>
</body>
<script>
const handleLogin = () => {
const name = document.getElementById('userName').value;
const psd = document.getElementById('userPsd').value;
if (!name || !psd) {
alert('请输入用户名和密码')
return false;
}
fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json', // ✅ 必须设置
},
body: JSON.stringify({
name,
psd
})
}).then(res => res.json()).then(res => {
if (res.code === 200) {
document.getElementById('userLogin').classList.add('hidden');
document.getElementById('userContent').classList.remove('hidden');
return;
}
alert(res.msg);
})
}
const handleLogout = () => {
fetch('/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json', // ✅ 必须设置
},
credentials: 'include'
}).then(res => res.json()).then(res => {
if (res.code === 200) {
document.getElementById('userLogin').classList.remove('hidden');
document.getElementById('userContent').classList.add('hidden');
alert('登出成功')
return;
}
alert(res.msg);
})
}
const transfer = () => {
const money = document.getElementById('money').value;
if (!money) {
alert('请输入金额');
return false;
}
fetch('/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json', // ✅ 必须设置
},
body: JSON.stringify({
money
})
}).then(res => res.json()).then(res => {
console.log('🍀🍀🍀🍀', res)
if (res.code === 200) {
alert('转账成功');
return;
} else {
alert(res.msg)
}
})
}
</script>
</html>
银行网站服务器
javascript
const Koa = require('koa');
const Router = require('@koa/router');
// koa 解析请求体,user=123 => {user: '123'}
const bodyParser = require('koa-bodyparser')
// koa 静态服务器,用于托管 bank.html 静态页面
const serve = require('koa-static');
// koa 维护登录状态,在 koa-session 的实现中,cookie 和 session 是一一对应的。即本地 cookie 改了,就会认证失败。
const session = require('koa-session').default;
// koa 跨域
const cors = require('@koa/cors');
// 配置 https 服务器
const https = require('https');
const fs = require('fs');
const path = require('path');
// 实例化
const app = new Koa();
const router = new Router();
// session 配置
app.keys = ['sercret-bank-key'];
app.use(bodyParser())
// cors 配置
app.use(cors({
origin: '*',
credentials: true, // 允许发送 Cookie
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法
maxAge: 86400,
}))
// 手动设置 cors
app.use(session({
key: 'koa.sess',
maxAge: 86400000, // 1天,ms
httpOnly: true, // 防止 xss 攻击
secure: true, // 启用 HTTPS
signed: true, // 防止篡改
sameSite: 'None', // 允许跨域
}, app))
app.use(serve(path.join(__dirname)))
/**
* API 实现
*/
router.all('/login', (ctx) => {
const {
name,
psd
} = ctx.request.body;
console.log('🍀🍀🍀🍀', name, psd)
if (name !== 'chp' || psd !== '123') {
ctx.body = {
code: 0,
msg: '密码错误,登录失败'
}
} else {
ctx.session.user = name;
ctx.body = {
code: 200,
msg: '登录成功'
}
}
})
router.all('/logout', (ctx) => {
ctx.session = null;
// 手动清除 cookie
ctx.cookies.set('koa.sess', null, { expires: new Date(0) });
ctx.cookies.set('koa.sess.sig', null, { expires: new Date(0) })
ctx.body = {
code: 200,
msg: '退出登录成功'
}
})
// 这里不该放 get 过去,否则用户给出一个链接,也能进行转账操作
router.all('/transfer', (ctx) => {
if (!ctx.session.user) {
ctx.body = {
status: 403,
code: 0,
msg: '请先登录'
}
return;
}
const { money } = ctx.request.body;
ctx.body = {
status: 200,
code: 200,
msg: `转账成功,转走了${money}元`
}
console.log('🍀🍀🍀🍀 转账成功,转走了', money);
})
app.use(router.routes()).use(router.allowedMethods());
// 以下没效果了,给 koa-session 提了个 issue,等解决,官方文档骗人!
app.on('session:missed', (err, ctx) => {
console.log('🍀🍀🍀🍀', 'ctx, sid')
})
app.on('session:invalid', (err, ctx) => {
console.log('🍀🍀🍀🍀', 'session:missed')
})
app.on('session:expired', (err, ctx) => {
console.log('🍀🍀🍀🍀', 'session:missed')
})
// https 证书、私钥配置
const options = {
key: fs.readFileSync('./bank.com+1-key.pem'),
cert: fs.readFileSync('./bank.com+1.pem')
}
https.createServer(options, app.callback()).listen(8088, () => {
console.log('🍀🍀🍀🍀', '运行静态页面 + 对应 api 接口')
})
csrf攻击,第三方网站
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=, initial-scale=1.0"
>
<title>CSRF攻击网页,嵌入 bank.com 的请求</title>
</head>
<body>
<h1>
CSRF攻击网页,嵌入 bank.com 的请求
</h1>
<form
action="https://bank.com:8088/transfer"
method="POST"
>
<input
name="money"
value="10000"
>
<input
type="submit"
value="提交"
>
</form>
</body>
<script>
// js 的 csrf 攻击
fetch('https://bank.com:8088/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
money: 10000
}),
credentials: 'include'
}).then(res => res.json()).then(res => {
console.log('🍀🍀🍀🍀', res)
})
</script>
</html>
csrf服务器
ini
const koa = require('koa');
const fs = require('fs');
const serve = require('koa-static')
const https = require('https');
const options = {
key: fs.readFileSync('./hack.com+1-key.pem'),
cert: fs.readFileSync('./hack.com+1.pem')
};
const app = new koa();
app.use(serve(__dirname))
https.createServer(options, app.callback()).listen(1111, () => {
console.log('🍀🍀🍀🍀', '运行')
})
验证效果
打开 switchhost
,修改 host 指向,并且使用 mkcert
生成 bank.com``hacker.com
的 ssl证书
。
swithhost配置:
127.0.0.1 bank.com
127.0.0.1 hacker.com
- 在
https://bank.com
完成登录,设置cookie
- 进入
https://hacker.com
执行转账 - 发现转账成功。
csrf 的成功的关键前提,网站使用 cookie 作为鉴权依据,且 cookie 安全配置薄弱。
- cookie 的
samesite
为None
或者Lax
( 注意,lax 实际上也不安全,它允许攻击者用<a>https://bank.com?option=transfer</a>
的方式携带 cookie,从而在这次点击中,完成相应攻击,如果 api 开发者允许 get 请求,且该请求为【关键操作】,可能带来巨大损失 ) - 服务器 cors 配置【网站白名单】为
*
,应该明确指定受信任域名。(Access-Control-Allow-Origin) - 服务器 cors 配置允许携带 cookie
credentials
为true
。(Access-Control-Allow-Credentials)
关于 CSRF 钓鱼邮件
在企业工作的时候,经常被提到【不安全的邮件,里面的链接不要乱点】。
其中的原因之一就是,防止被 CSRF 攻击。
如果关键操作(如【转账】)允许使用 GET 请求),攻击者仍可通过伪造 <a>
链接或诱导用户点击恶意页面。
🍀 安全、便捷,两者有时无法共存。
SameSite=Strict
,跨站导航(如shop.com
跳转到bank.com
)不会发送 Cookie
。
导致用户需要重新登陆。
这是为了彻底防御 CSRF
攻 击,但牺牲了用户体验。




防御手段
CSRF 攻击成功的【充分】【必要】条件:
SameSite: None
或者SameSite: Lax
的同时,关键操作用GET
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
【基本配置】严格设置 Cookie 的 SameSite
原理:控制浏览器跨站是否携带 Cookie
。
选项:
SameSite=None
允许跨站携带 Cookie。此时 secure 必须配置为 true(https)。SameSite=Lax
【默认值是这个】允许部分操作携带(a链接GET导航允许)。SameSite=Strict
完全禁止跨站携带 Cookie。
【万恶之源1】
由于一个公司,可能有多个【域名】【站名】。
所以有些公司图方便,就设置成 None 了。
有时就要这么配,那么往下看。
【基本配置】CORS限制好(针对API)
原理:通过Access-Control-Allow-Origin
设置好白名单。
但是【内网攻击】没法防范,假设你们内部员工有问题。还是可能被绕过。
发你一个链接,不要乱点。
rust
add_header 'Access-Control-Allow-Origin' 'https://trusted.com';
add_header 'Access-Control-Allow-Credentials' 'true';
一般的偷懒配置,导致了 CSRF 。
【万恶之源2】
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
有时就要这么配,那么往下看。
关键操作不要用 GET

【提升 CSRF 的成本】
若是关键操作直接用 GET
,又允许跨站携带 cookie
,且不做任意校验,那攻击起来太简单了。
一个钓鱼邮件足矣。
使用 CSRF token 进行防御
比较主流的方案。你会在以下地方看到
- 主流的电商平台(如京东、淘宝):下单、支付等敏感操作必须携带 cookie
- 银行系统(网银转账):大多数他们还有【二次校验】【短信验证码】
- 社交网站
【为何主流】
- 简单有效(攻击者无法获取这个
token
,受浏览器同源策略影响,无法获取 LocalStorage 的内容(通常会加密存在 localStorage 里) - 兼容性强、适配各种框架
- 无侵入性,不需要额外验证码
【伪代码实现】
登录成功
ini
ctx.session.user = name;
ctx.session.loginStatus = true;
ctx.session.csrfToken = Math.random().toString(36).slice(2); // 随机字符串
ctx.body = {
code: 200,
msg: '登录成功',
csrfToken: ctx.session.csrfToken
}
转账校验
css
if (!token) {
ctx.body = {
code: 0,
msg: '没有权限标识,请先登录'
}
return;
}
验证 Origin
和Referer
头
原理:检查【请求来源】是否来自受信任的网址。
koa检测origin中间件
ini
const allowedDomains = ['https://your-trusted-domain.com'];
module.exports = async (ctx, next) => {
const origin = ctx.get('Origin');
const referer = ctx.get('Referer');
// 允许没有 Origin/Referer 的同源请求(如直接访问)
if (!origin && !referer) {
await next();
return;
}
// 校验 Origin
if (origin && !allowedDomains.includes(origin)) {
ctx.status = 403;
ctx.body = { error: '非法请求来源 (Origin)' };
return;
}
// 校验 Referer
if (referer) {
try {
const refererDomain = new URL(referer).origin;
if (!allowedDomains.includes(refererDomain)) {
ctx.status = 403;
ctx.body = { error: '非法请求来源 (Referer)' };
return;
}
} catch (e) {
ctx.status = 403;
ctx.body = { error: 'Referer 解析失败' };
return;
}
}
await next();
};
关键操作增加二次验证
最有效的,添加手机验证码认证,我愿称之为永远的神!!防范 99.99%。
- 密码/短信验证码
- 人脸识别/生物识别
- 邮箱确认
即使 cookie 被盗了,仍需客户主动认证。
如果你直接弃用了Cookie
为了防止钥匙被盗,被复制,于是我们干脆不用"钥匙"开门了?
魔高一尺、道高一丈,遵守原则才不会两行泪。
始终防御 XSS
哪怕不使用 Cookie 鉴权,XSS 可以获取 LocalStorage
中的 token,需要确保
- 避免动态插入
html
- 设置 CSP 请求头,只允许可信范围的 JS 执行。
HTTPS必须
防止 token 被中间人获取
短期有效的 token
token的有效期设置的尽量短(1小时、15分钟等)。
可以增加 refreshToken 来提升用户体验