前言
随着前端技术的不断发展和应用,越来越多的网站开始采用前后端分离的方式进行开发,前端通过 AJAX 等方式向后端请求数据和服务。CSRF(Cross-site request forgery)跨站请求伪造攻击因此也变得越来越常见。尤其是对于一些敏感的操作(如转账、修改密码等),如果没有做好CSRF防范,将会产生巨大的危害。本文将介绍在 Koa2 中如何进行 CSRF 防范。
什么是 CSRF 攻击
首先,我们先来了解一下什么是 CSRF 攻击。当用户被冒充登录了一个网站后,在该网站中,任何一个攻击者的WEB页面都可以实现伪造用户操作请求的目的,而这个请求是伪造的,是由攻击者构造的,但却没有被用户明确授权,这就是 CSRF 攻击。
CSRF 攻击的原理
攻击者在第三方网站上通过特殊的方式引导用户去访问受害网站,并在该网站上进行一些操作,这些操作会利用用户之前在受害者网站上的登录凭证。例如,假设有一个银行网站,它的转账功能如下:
app.post('/transfer', async (ctx, next) => {
const { amount, to } = ctx.request.body;
// 查询余额,转账操作等
});这时,攻击者可以构造一个表单,提交到第三方网站上:
<form id="hackForm" action="http://example.com/transfer" method="POST">
<input type="hidden" name="amount" value="100000">
<input type="hidden" name="to" value="hackersaccount">
</form>
<script>
document.getElementById('hackForm').submit();
</script>当用户点击了第三方网站上的某个诱导链接并访问到该网页后,该表单便会被自动触发并在用户不知情的情况下向银行网站发起请求,从而触发转账操作。
CSRF 防御方法
- 随机 Token:在表单提交的时候在请求中放一个 token,服务端需要验证 CSRF Token 是否合法。这种方法是最常见的 CSRF 防御方法。
- Same-Site Cookies: Same-Site Cookie 是一种 Cookie 的属性,它可以让浏览器限制第三方 Cookie 的发送,这也可以间接的防止 CSRF 攻击。
- Referer 参数校验:服务端可以通过 Referer 这个请求头来判断请求是从哪里发出的。如果是其他网站的请求,就直接拒绝。
- Anti-CSRF 功能:如 OWASP CSRF Prevention Cheat Sheet 中所述,实现当前最流行且最常用的技术。
在本文中,我们将使用最常见的随机 Token 的方式来进行 CSRF 防范。
随机 Token 的方式实现 CSRF 防范
生成 Token
Token 的生成可以使用 UUID(npm包 uuid) 进行生成,安装 uuid 包,可以使用以下命令:
npm install uuid --save-dev
生成 Token 代码如下:
// www.javascriptcn.com code example
const uuidv4 = require('uuid/v4');
const state = await getState(ctx);
state.csrf = uuidv4();
function getState(ctx) {
if (ctx.session.state) return ctx.session.state;
const state = {};
ctx.session.state = state;
return state;
}通过一个简单的 getState 函数返回 state,state 中存放了 csrf。如果 csrf 不在 state 中,则生成一个 uuid,并赋值给 state,最后将 state 存入 session 中。
校验 Token
校验 Token 只需要在请求头中提交 csrf,服务器在校验并响应请求时,取出 state 中的 csrf,并进行比较即可。
需要注意的是,在每次操作需要校验 CSRF Token 的请求上添加了 header:
// www.javascriptcn.com code example
ctx.set('Content-Security-Policy', "default-src 'self'");
ctx.set('X-Frame-Options', 'SAMEORIGIN');
ctx.set('X-XSS-Protection', '1; mode=block');
ctx.set('X-Content-Type-Options', 'nosniff');
if (!ctx.request.get('Origin')) return await next();
ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Methods', 'POST');
ctx.set('Access-Control-Allow-Headers', 'Authorization, Content-Type, CSRF-Token, X-Requested-With');
ctx.set('Access-Control-Allow-Credentials', 'true');
if (ctx.method !== 'POST') return await next();
const state = getState(ctx);
if (!ctx.request.body.csrf || ctx.request.body.csrf !== state.csrf) {
return ctx.throw(403);
}
state.csrf = uuidv4();在上述代码中,首先是安全头(Content-Security-Policy、X-Frame-Options、X-XSS-Protection、X-Content-Type-Options),这些头的目的就是规范服务端的响应,告诉浏览器如何去保护自己。接下来是允许跨域的操作(Access-Control-Allow-*),需要做到前后端分离,就不可避免地涉及到跨域问题,因此要在服务端允许跨域请求。最后,当请求为 POST 请求时,要进行 CSRF Token 的校验,没有通过校验则返回 403,并重新生成 Token。
上述代码,也可以使用 Koa-csrf 这个库来实现。
CSRF 防范示例代码
// www.javascriptcn.com code example
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const session = require('koa-session');
const uuidv4 = require('uuid/v4');
const app = new Koa();
app.keys = ['some secret hurr'];
const CONFIG = {
key: 'koa:sess', maxAge: 86400000, autoCommit: true, overwrite: true, httpOnly: true, signed: true, rolling: false, renew: false, // don't 重复更新
};
app.use(session(CONFIG, app));
app.use(bodyParser());
app.use(async (ctx, next) => {
const state = await getState(ctx);
state.csrf = state.csrf || uuidv4();
await next();
});
app.use(async (ctx, next) => {
const state = await getState(ctx);
if (ctx.method === 'POST' && ctx.path !== '/api/login') {
if (!ctx.request.body.csrf || ctx.request.body.csrf !== state.csrf) {
ctx.throw(403);
}
state.csrf = uuidv4();
}
await next();
});
app.use(async (ctx) => {
const state = await getState(ctx);
ctx.body = `
<form action="/api/transfer" method="POST">
<input type="number" name="amount" value="100">
<input type="text" name="to" value="hacker">
<input type="hidden" name="csrf" value="${state.csrf}">
<button type="submit">转账</button>
</form>
`;
});
function getState(ctx) {
if (ctx.session.state) return ctx.session.state;
const state = {};
ctx.session.state = state;
return state;
}
app.listen(3000, () => {
console.log('Server is listening on port 3000!');
});小结
本文介绍了在 Koa2 中实现 CSRF 防范的最佳实践——通过随机 Token 来生成 CSRF Token。这种方法既简单又有效,并可以用于绝大部分 Web 应用中。在实际开发过程中,我们应该对网站中的敏感操作实施 CSRF 防范,以加强安全性。
来源:JavaScript中文网 ,转载请注明来源 https://www.javascriptcn.com/post/67d68464a941bf7134c4d511