CSRF防护
CSRF攻击原理
CSRF(Cross-Site Request Forgery)跨站请求伪造是一种常见的Web安全威胁。攻击者诱导用户在已认证的Web应用中执行非预期的操作。这种攻击利用了Web应用对用户浏览器的信任机制,当用户登录某个网站后,浏览器会自动携带该网站的cookie信息,攻击者通过构造恶意请求让用户在不知情的情况下执行操作。
典型的CSRF攻击流程:
- 用户登录可信网站A,服务器返回包含身份验证信息的cookie
- 用户未登出网站A的情况下访问恶意网站B
- 网站B包含向网站A发起请求的代码(如自动提交表单)
- 浏览器自动携带网站A的cookie发送请求
- 网站A服务器认为这是用户的合法请求
// 恶意网站可能构造的自动提交表单
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="amount" value="10000"/>
<input type="hidden" name="toAccount" value="attacker"/>
</form>
<script>
document.forms[0].submit();
</script>
Node.js中的CSRF防护机制
在Node.js生态中,有多种防护CSRF攻击的方案。最常用的是同步令牌模式(Synchronizer Token Pattern),其核心思想是为每个用户会话生成唯一的令牌,要求每个状态修改请求必须包含该令牌。
Express框架中常用的CSRF防护中间件:
const express = require('express');
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
const app = express();
app.use(cookieParser());
app.use(csrf({ cookie: true }));
// 获取CSRF令牌的接口
app.get('/csrf-token', (req, res) => {
res.json({ token: req.csrfToken() });
});
实现双重提交Cookie方案
双重提交Cookie是另一种有效的CSRF防护方法,它不依赖服务器存储令牌,而是要求客户端同时通过cookie和请求体/头传递相同的随机值。
实现步骤:
- 服务器在响应中设置包含随机令牌的cookie
- 客户端需要在后续请求中从cookie读取该值并附加到请求中
- 服务器验证cookie值和请求中的值是否匹配
// 服务器端实现
app.use((req, res, next) => {
const csrfToken = crypto.randomBytes(16).toString('hex');
res.cookie('XSRF-TOKEN', csrfToken, {
httpOnly: false, // 允许前端JavaScript读取
secure: process.env.NODE_ENV === 'production'
});
req.csrfToken = csrfToken;
next();
});
// 验证中间件
app.use((req, res, next) => {
if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
const cookieToken = req.cookies['XSRF-TOKEN'];
const bodyToken = req.body._csrf || req.headers['x-csrf-token'];
if (!cookieToken || cookieToken !== bodyToken) {
return res.status(403).send('CSRF token验证失败');
}
}
next();
});
同源策略与CORS配置
正确配置CORS(跨源资源共享)可以辅助防止CSRF攻击。虽然CORS不是专门用于CSRF防护的机制,但合理的配置可以减少攻击面。
Node.js中CORS中间件的安全配置:
const cors = require('cors');
app.use(cors({
origin: ['https://trusted-domain.com'], // 明确指定允许的源
methods: ['GET', 'POST', 'OPTIONS'], // 限制允许的HTTP方法
allowedHeaders: ['Content-Type', 'X-CSRF-Token'], // 限制允许的头部
credentials: true, // 允许携带凭据
maxAge: 86400 // 预检请求缓存时间
}));
内容安全策略(CSP)增强防护
内容安全策略可以限制页面加载的外部资源,间接防止某些CSRF攻击向量。通过限制脚本来源,可以阻止恶意脚本执行。
Node.js中设置CSP头部的示例:
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", "trusted.cdn.com"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:"],
connectSrc: ["'self'"],
formAction: ["'self'"], // 限制表单提交目标
frameAncestors: ["'none'"] // 防止点击劫持
}
}));
前端与后端的协同防护
完整的CSRF防护需要前后端协同工作。前端需要正确处理CSRF令牌,确保敏感操作请求包含有效的令牌。
React中的实现示例:
import { useState, useEffect } from 'react';
function TransferForm() {
const [csrfToken, setCsrfToken] = useState('');
useEffect(() => {
// 从cookie中获取CSRF令牌
const token = document.cookie
.split('; ')
.find(row => row.startsWith('XSRF-TOKEN='))
?.split('=')[1];
setCsrfToken(token || '');
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
const response = await fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
amount: e.target.amount.value,
toAccount: e.target.account.value,
_csrf: csrfToken
})
});
// 处理响应...
};
return (
<form onSubmit={handleSubmit}>
<input type="hidden" name="_csrf" value={csrfToken} />
<input type="number" name="amount" />
<input type="text" name="account" />
<button type="submit">转账</button>
</form>
);
}
会话管理与CSRF防护
会话管理策略直接影响CSRF防护的有效性。需要考虑会话过期时间、cookie属性和令牌刷新机制。
安全的会话配置示例:
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'strict', // 严格的同站策略
maxAge: 24 * 60 * 60 * 1000 // 24小时
},
rolling: true // 每次请求刷新会话过期时间
}));
针对API的特殊考虑
纯API服务可能需要不同的CSRF防护策略,特别是当客户端是移动应用或第三方服务时。可以考虑使用基于JWT的防护方案。
JWT CSRF防护实现:
const jwt = require('jsonwebtoken');
// 签发双重用途令牌
app.post('/login', (req, res) => {
const user = authenticate(req.body);
const csrfToken = crypto.randomBytes(16).toString('hex');
const accessToken = jwt.sign({
sub: user.id,
csrf: csrfToken
}, process.env.JWT_SECRET, { expiresIn: '1h' });
res.json({
accessToken,
csrfToken
});
});
// 验证中间件
app.use((req, res, next) => {
if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const headerToken = req.headers['x-csrf-token'];
if (decoded.csrf !== headerToken) {
return res.status(403).send('Invalid CSRF token');
}
req.user = { id: decoded.sub };
next();
} catch (err) {
res.status(401).send('Invalid token');
}
} else {
next();
}
});
性能与安全权衡
CSRF防护可能对性能产生影响,特别是在高并发场景下。需要考虑令牌生成和验证的效率优化。
令牌缓存优化示例:
const nodeCache = require('node-cache');
const csrfCache = new nodeCache({ stdTTL: 3600 });
// 生成并缓存令牌
app.use((req, res, next) => {
if (req.session) {
const cachedToken = csrfCache.get(req.sessionID);
const token = cachedToken || crypto.randomBytes(16).toString('hex');
if (!cachedToken) {
csrfCache.set(req.sessionID, token);
}
req.csrfToken = token;
res.locals.csrfToken = token;
}
next();
});
// 验证中间件优化
app.use((req, res, next) => {
if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
const sessionToken = csrfCache.get(req.sessionID);
const requestToken = req.body._csrf || req.headers['x-csrf-token'];
if (!sessionToken || sessionToken !== requestToken) {
return res.status(403).send('CSRF验证失败');
}
}
next();
});
常见漏洞场景分析
即使实施了CSRF防护,某些场景下仍可能存在漏洞,需要特别注意。
-
JSON API漏洞: 当API接受JSON请求时,如果仅依赖cookie进行CSRF防护可能无效,因为浏览器会自动携带cookie。
防护方案:
app.use(express.json()); app.post('/api/json-endpoint', (req, res) => { const csrfToken = req.headers['x-csrf-token']; const cookieToken = req.cookies['XSRF-TOKEN']; if (!csrfToken || csrfToken !== cookieToken) { return res.status(403).json({ error: 'CSRF token required' }); } // 处理正常请求... });
-
子域名漏洞: 如果主站和子域共享cookie,攻击者可能利用子域发起CSRF攻击。
解决方案:
// 设置cookie时明确指定domain和sameSite res.cookie('XSRF-TOKEN', token, { domain: 'example.com', sameSite: 'strict', secure: true });
-
登录CSRF: 攻击者可能伪造登录请求,使用户以攻击者账户登录。
防护措施:
// 登录表单也需要CSRF防护 app.get('/login', (req, res) => { res.render('login', { csrfToken: req.csrfToken() }); });
自动化测试CSRF防护
确保CSRF防护机制有效需要自动化测试。可以使用Jest和Supertest编写测试用例。
测试示例:
const request = require('supertest');
const app = require('../app');
describe('CSRF防护测试', () => {
let agent;
let csrfToken;
let cookie;
beforeEach(async () => {
agent = request.agent(app);
// 获取CSRF令牌
const response = await agent.get('/csrf-token');
csrfToken = response.body.token;
cookie = response.headers['set-cookie'];
});
test('缺少CSRF令牌应返回403', async () => {
const res = await agent
.post('/protected-route')
.set('Cookie', cookie)
.send({ data: 'test' });
expect(res.statusCode).toBe(403);
});
test('有效CSRF令牌应通过验证', async () => {
const res = await agent
.post('/protected-route')
.set('Cookie', cookie)
.set('X-CSRF-Token', csrfToken)
.send({ data: 'test' });
expect(res.statusCode).toBe(200);
});
test('无效CSRF令牌应拒绝请求', async () => {
const res = await agent
.post('/protected-route')
.set('Cookie', cookie)
.set('X-CSRF-Token', 'invalid-token')
.send({ data: 'test' });
expect(res.statusCode).toBe(403);
});
});
与其他安全措施的协同
CSRF防护需要与其他安全措施协同工作,形成纵深防御体系。
-
与XSS防护结合: XSS漏洞可能绕过CSRF防护,需要同时防范。
// 使用helmet设置安全头部 app.use(helmet()); // 输入过滤 app.use(express.urlencoded({ extended: true })); app.use((req, res, next) => { // 简单的XSS过滤 for (const key in req.body) { if (typeof req.body[key] === 'string') { req.body[key] = req.body[key].replace(/</g, '<').replace(/>/g, '>'); } } next(); });
-
与速率限制结合: 防止CSRF令牌被暴力破解。
const rateLimit = require('express-rate-limit'); const csrfLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15分钟 max: 100, // 每个IP最多100次请求 message: '请求过于频繁' }); app.use('/csrf-token', csrfLimiter);
-
与敏感操作验证结合: 关键操作需要额外验证。
// 转账前需要验证密码 app.post('/transfer', (req, res) => { if (!verifyPassword(req.user.id, req.body.password)) { return res.status(403).send('密码验证失败'); } // 执行转账... });
实际部署注意事项
在生产环境中部署CSRF防护需要考虑更多实际因素。
-
负载均衡环境: 多服务器环境下需要确保CSRF令牌同步。
// 使用Redis存储会话和CSRF令牌 const redis = require('redis'); const client = redis.createClient({ host: process.env.REDIS_HOST, password: process.env.REDIS_PASS }); app.use(session({ store: new RedisStore({ client }), // 其他配置... }));
-
CDN和代理配置: 确保安全头部不被CDN剥离。
# Nginx配置示例 location / { proxy_pass http://node_server; proxy_set_header X-Forwarded-Proto $scheme; proxy_cookie_path / "/; secure; HttpOnly; SameSite=Strict"; }
-
混合内容问题: 确保全站HTTPS,避免混合内容降低安全性。
// 强制HTTPS中间件 app.use((req, res, next) => { if (req.headers['x-forwarded-proto'] !== 'https' && process.env.NODE_ENV === 'production') { return res.redirect(`https://${req.hostname}${req.url}`); } next(); });
框架特定实现细节
不同Node.js框架可能有不同的CSRF防护实现方式。
-
Express csurf中间件:
const csrf = require('csurf'); const csrfProtection = csrf({ cookie: true }); // 获取令牌的路由不需要保护 app.get('/form', csrfProtection, (req, res) => { res.render('form', { csrfToken: req.csrfToken() }); }); // 提交表单的路由需要保护 app.post('/process', csrfProtection, (req, res) => { res.send('表单已提交'); });
-
Koa实现:
const Koa = require('koa'); const CSRF = require('koa-csrf'); const session = require('koa-session'); const app = new Koa(); app.keys = ['some secret key']; app.use(session(app)); // 添加CSRF中间件 app.use(new CSRF({ invalidSessionSecretMessage: 'Invalid session secret', invalidSessionSecretStatusCode: 403, invalidTokenMessage: 'Invalid CSRF token', invalidTokenStatusCode: 403, excludedMethods: ['GET', 'HEAD', 'OPTIONS'], disableQuery: false })); // 在路由中获取令牌 app.use(async (ctx, next) => { if (ctx.method === 'GET') { ctx.state.csrf = ctx.csrf; } await next(); });
-
NestJS实现:
import { Module, MiddlewareConsumer } from '@nestjs/common'; import * as csurf from 'csurf'; @Module({}) export class AppModule { configure(consumer: MiddlewareConsumer) { consumer .apply( cookieParser(), csurf({ cookie: true }) ) .forRoutes('*'); } } // 在控制器中获取令牌 @Get('csrf-token') getCsrfToken(@Req() req) { return { csrfToken: req.csrfToken() }; }
移动端API的特殊处理
移动应用调用API时,传统的CSRF防护可能需要调整。
- 自定义头部方案:
// 中间件验证自定义头部 app.use((req, res, next) => { if (['POST', 'PUT', 'DELETE'].includes(req.method)) { const appHeader = req.headers['x-app-version']; const apiKey = req.headers['x-api-key']; if (!appHeader || !apiKey) { return res.status(403).json({ error: '缺少必要的安全头部' }); } // 验证API密钥... } next();
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn