CSRF防护
概览
使用同步器令牌、双重提交Cookie、SameSite Cookie属性和自定义头部实现全面的跨站请求伪造保护。用于构建表单和状态更改操作时。
何时使用
- 表单提交
- 状态更改操作
- 认证系统
- 支付处理
- 账户管理
- 任何POST/PUT/DELETE请求
实施示例
1. Node.js/Express CSRF防护
// csrf-protection.js
const crypto = require('crypto');
const csrf = require('csurf');
class CSRFProtection {
constructor() {
this.tokens = new Map();
this.tokenExpiry = 3600000; // 1小时
}
/**
* 生成CSRF令牌
*/
generateToken() {
return crypto.randomBytes(32).toString('hex');
}
/**
* 为会话创建令牌
*/
createToken(sessionId) {
const token = this.generateToken();
const expiry = Date.now() + this.tokenExpiry;
this.tokens.set(sessionId, {
token,
expiry
});
return token;
}
/**
* 验证CSRF令牌
*/
validateToken(sessionId, token) {
const stored = this.tokens.get(sessionId);
if (!stored) {
return false;
}
if (Date.now() > stored.expiry) {
this.tokens.delete(sessionId);
return false;
}
return crypto.timingSafeEqual(
Buffer.from(stored.token),
Buffer.from(token)
);
}
/**
* Express中间件
*/
middleware() {
return (req, res, next) => {
// 跳过GET, HEAD, OPTIONS
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next();
}
const token = req.headers['x-csrf-token'] || req.body._csrf;
const sessionId = req.session?.id;
if (!token) {
return res.status(403).json({
error: 'csrf_token_missing',
message: 'CSRF令牌是必需的'
});
}
if (!this.validateToken(sessionId, token)) {
return res.status(403).json({
error: 'csrf_token_invalid',
message: 'CSRF令牌无效或已过期'
});
}
next();
};
}
}
// Express设置与csurf包
const express = require('express');
const session = require('express-session');
const cookieParser = require('cookie-parser');
const app = express();
// 会话配置
app.use(cookieParser());
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600000
}
}));
// CSRF保护中间件
const csrfProtection = csrf({
cookie: {
httpOnly: true,
secure: true,
sameSite: 'strict'
}
});
app.use(csrfProtection);
// 提供令牌给模板
app.use((req, res, next) => {
res.locals.csrfToken = req.csrfToken();
next();
});
// API端点获取CSRF令牌
app.get('/api/csrf-token', (req, res) => {
res.json({
csrfToken: req.csrfToken()
});
});
// 受保护的路由
app.post('/api/transfer', csrfProtection, (req, res) => {
const { amount, toAccount } = req.body;
// 处理转账
res.json({
message: '转账成功',
amount,
toAccount
});
});
// CSRF错误处理程序
app.use((err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN') {
return res.status(403).json({
error: 'csrf_error',
message: 'CSRF令牌无效'
});
}
next(err);
});
module.exports = { CSRFProtection, csrfProtection };
2. 双重提交Cookie模式
// double-submit-csrf.js
const crypto = require('crypto');
class DoubleSubmitCSRF {
/**
* 生成CSRF令牌并设置Cookie
*/
static generateAndSetToken(res) {
const token = crypto.randomBytes(32).toString('hex');
// 设置CSRF Cookie
res.cookie('XSRF-TOKEN', token, {
httpOnly: false, // 允许JS读取双重提交
secure: true,
sameSite: 'strict',
maxAge: 3600000
});
return token;
}
/**
* 中间件验证双重提交
*/
static middleware() {
return (req, res, next) => {
// 跳过GET, HEAD, OPTIONS
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next();
}
const cookieToken = req.cookies['XSRF-TOKEN'];
const headerToken = req.headers['x-xsrf-token'];
if (!cookieToken || !headerToken) {
return res.status(403).json({
error: 'csrf_token_missing'
});
}
// 比较令牌(时间安全)
if (!crypto.timingSafeEqual(
Buffer.from(cookieToken),
Buffer.from(headerToken)
)) {
return res.status(403).json({
error: 'csrf_token_mismatch'
});
}
next();
};
}
}
// Express设置
const app = express();
const cookieParser = require('cookie-parser');
app.use(cookieParser());
app.use(express.json());
// 登录时生成令牌
app.post('/api/login', async (req, res) => {
// 认证用户
const token = DoubleSubmitCSRF.generateAndSetToken(res);
res.json({
message: '登录成功',
csrfToken: token
});
});
// 受保护的路由
app.use('/api/*', DoubleSubmitCSRF.middleware());
app.post('/api/update-profile', (req, res) => {
// 更新配置文件
res.json({ message: '配置文件已更新' });
});
3. Python Flask CSRF防护
# csrf_protection.py
from flask import Flask, session, request, jsonify
from flask_wtf.csrf import CSRFProtect, generate_csrf, validate_csrf
from functools import wraps
import secrets
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['WTF_CSRF_TIME_LIMIT'] = 3600 # 1小时
app.config['WTF_CSRF_SSL_STRICT'] = True
csrf = CSRFProtect(app)
# Cookie配置
app.config.update(
SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE='Strict'
)
@app.before_request
def csrf_protect():
"""验证状态更改方法的CSRF令牌"""
if request.method in ['POST', 'PUT', 'DELETE', 'PATCH']:
token = request.headers.get('X-CSRF-Token') or request.form.get('csrf_token')
if not token:
return jsonify({'error': 'CSRF令牌缺失'}), 403
try:
validate_csrf(token)
except:
return jsonify({'error': 'CSRF令牌无效'}), 403
@app.route('/api/csrf-token', methods=['GET'])
def get_csrf_token():
"""向客户端提供CSRF令牌"""
token = generate_csrf()
return jsonify({'csrfToken': token})
@app.route('/api/transfer', methods=['POST'])
def transfer_funds():
"""受保护的端点"""
data = request.get_json()
return jsonify({
'message': '转账成功',
'amount': data.get('amount')
})
# 自定义CSRF装饰器
def require_csrf(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if request.method in ['POST', 'PUT', 'DELETE']:
token = request.headers.get('X-CSRF-Token')
if not token:
return jsonify({'error': 'CSRF令牌所需'}), 403
try:
validate_csrf(token)
except:
return jsonify({'error': 'CSRF令牌无效'}), 403
return f(*args, **kwargs)
return decorated_function
@app.route('/api/sensitive-action', methods=['POST'])
@require_csrf
def sensitive_action():
return jsonify({'message': '操作完成'})
if __name__ == '__main__':
app.run(ssl_context='adhoc')
4. 前端CSRF实现
// csrf-client.js
class CSRFClient {
constructor() {
this.token = null;
this.tokenExpiry = null;
}
/**
* 从服务器获取CSRF令牌
*/
async fetchToken() {
const response = await fetch('/api/csrf-token', {
credentials: 'include'
});
const data = await response.json();
this.token = data.csrfToken;
this.tokenExpiry = Date.now() + 3600000; // 1小时
return this.token;
}
/**
* 获取有效令牌(如果需要则获取)
*/
async getToken() {
if (!this.token || Date.now() > this.tokenExpiry) {
await this.fetchToken();
}
return this.token;
}
/**
* 发起受保护的请求
*/
async request(url, options = {}) {
const token = await this.getToken();
const headers = {
'Content-Type': 'application/json',
'X-CSRF-Token': token,
...options.headers
};
return fetch(url, {
...options,
headers,
credentials: 'include'
});
}
/**
* 带CSRF令牌的POST请求
*/
async post(url, data) {
return this.request(url, {
method: 'POST',
body: JSON.stringify(data)
});
}
/**
* 带CSRF令牌的PUT请求
*/
async put(url, data) {
return this.request(url, {
method: 'PUT',
body: JSON.stringify(data)
});
}
/**
* 带CSRF令牌的DELETE请求
*/
async delete(url) {
return this.request(url, {
method: 'DELETE'
});
}
}
// 使用
const client = new CSRFClient();
async function transferFunds() {
try {
const response = await client.post('/api/transfer', {
amount: 1000,
toAccount: '123456'
});
const result = await response.json();
console.log('转账成功:', result);
} catch (error) {
console.error('转账失败:', error);
}
}
// React钩子用于CSRF
function useCSRF() {
const [token, setToken] = React.useState(null);
React.useEffect(() => {
async function fetchToken() {
const response = await fetch('/api/csrf-token');
const data = await response.json();
setToken(data.csrfToken);
}
fetchToken();
}, []);
return token;
}
// React表单中的使用
function TransferForm() {
const csrfToken = useCSRF();
const handleSubmit = async (e) => {
e.preventDefault();
await fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
amount: 1000,
toAccount: '123456'
})
});
};
return (
<form onSubmit={handleSubmit}>
<input type="hidden" name="_csrf" value={csrfToken} />
{/* 表单字段 */}
<button type="submit">转账</button>
</form>
);
}
5. 起源和Referer验证
// origin-validation.js
function validateOrigin(req, res, next) {
const allowedOrigins = [
'https://example.com',
'https://app.example.com'
];
const origin = req.headers.origin;
const referer = req.headers.referer;
// 检查Origin头部
if (origin && !allowedOrigins.includes(origin)) {
return res.status(403).json({
error: 'invalid_origin'
});
}
// 检查Referer头部作为回退
if (!origin && referer) {
const refererUrl = new URL(referer);
if (!allowedOrigins.includes(refererUrl.origin)) {
return res.status(403).json({
error: 'invalid_referer'
});
}
}
next();
}
// 应用于状态更改路由
app.use('/api/*', validateOrigin);
最佳实践
✅ 做
- 对所有状态更改操作使用CSRF令牌
- 设置SameSite=Strict在Cookie上
- 验证Origin/Referer头部
- 使用安全、随机的令牌
- 实施令牌过期
- 仅使用HTTPS
- 在AJAX请求中包含令牌
- 测试CSRF保护
❌ 不做
- 跳过认证请求的CSRF
- 使用GET进行状态更改
- 单独信任Origin头部
- 重用令牌
- 在localStorage中存储令牌
- 在没有验证的情况下允许CORS中的凭据
CSRF防护方法
- 同步器令牌: 服务器生成的令牌
- 双重提交Cookie: Cookie和头部匹配
- SameSite Cookies: 浏览器级保护
- 自定义头部: X-Requested-With
- 起源验证: 检查请求起源
防御层
- [ ] 实施CSRF令牌
- [ ] 配置SameSite Cookie
- [ ] 起源/Referer验证
- [ ] 自定义请求头部
- [ ] 令牌过期
- [ ] 安全Cookie标志
- [ ] 强制HTTPS