GX博客

分享个人 Full-Stack JavaScript 项目开发经验

使用koa-csrf防范跨站请求伪造攻击

跨站请求伪造(cross-site request forgery,CSRF)是指攻击者欺骗用户浏览器,让其以用户的名义运行操作。这种跨站请求依赖于浏览器同源策略中允许跨域写操作和跨域资源嵌入的开放规则,如链接、表单提交、css 的 url 和<img>嵌入等方式。

攻击示例的大致过程:用户使用同一浏览器访问两个站点。用户先在A站点登录,并获得会话凭据。然后访问B站点,其中B站点中预设了一个提交给A站点的包含恶意操作的<form>表单,并通过脚本的 submit 方法自动提交。当用户访问B站点时,将会使用此时用户在A站点的会话凭据来请求A站点,于是通过了身份验证并执行恶意操作。


通过检查 Referer 请求头来判断请求来源地址的方法并不是那么可靠,所以通常会使用添加校验 token 的方式来防范 CSRF。下面介绍 koa2 框架适用的 koa-csrf 模块是如何帮助我们实现这一功能。


安装 koa-csrf

通过 yarn 安装:

yarn add koa-csrf

使用 koa-csrf 的 CSRF 类实例化中间件:

const CSRF = require('koa-csrf');

module.exports = new CSRF({
    invalidTokenMessage: 'Invalid CSRF token',
    invalidTokenStatusCode: 403,
    excludedMethods: ['GET', 'HEAD', 'OPTIONS'],
    disableQuery: true
});

参数的说明如下:

  • invalidTokenMessage

    使 koa 抛出的错误信息内容,默认值为:'Invalid CSRF token'。它可以是一个接收 ctx 作为参数的函数,函数最后返回错误信息内容。

  • invalidTokenStatusCode

    验证失败时的响应状态码,默认值为:403(Forbidden)。跟 invalidTokenMessage 参数一样,它也会被传递给 ctx.throw,用于抛出错误和拒绝请求。

  • excludedMethods

    排除的请求方法,默认值为:['GET', 'HEAD', 'OPTIONS']。

  • disableQuery

    是否禁止通过查询字符串传递 _csrf 校验 token,默认值为 false。如果校验 token 出现在 URL 中,则可能会通过 Referer 泄露,应尽量把 Token 放在表单中,把敏感操作由 GET 改为 POST。


对敏感路由应用中间件

app.js 中添加 session 支持。

const session = require("koa-session");
// ...
app.keys = [process.env.COOKIE_SECRET];
app.use(session(CONFIG, app));
// ...

在路由级别应用 csrf 限制:

const router = require('koa-router')();
const controllers = require('../controllers');
const {authenticate} = require("../lib/middleware/auth");
const csrf = require('../lib/middleware/csrf');

router.post('/clear', authenticate, csrf, controllers.admin.clear.apiClear);

在路由控制器中获取校验 token 的值:

module.exports = async (ctx, next) => {
    // console.log(ctx.csrf);
    // .....
}

请求中提交校验 token 的字段名称为 _csrf。如果你的表单中包含文件上传,使用了 encType="multipart/form-data",开始时 koa-bodyparser 不能处理这样的请求体,则可以通过 x-csrf-token 请求头来提交校验 token,校验通过后再使用 koa-multer 来处理请求体。


原理分析

koa-csrf 存放在 session 中的 secret 和传送到窗体中的 token 都是通过csrf模块来创建和比对的。流程如下:

  1. 如果 session.secret 不存在,则先创建一个 18 字节(默认)长度的唯一 secret 存放在 session 中。
  2. 根据 secret 和 8 字节(默认)长度的随机盐值经过特定的哈希计算生成 token。token 包含两部分信息,一部分是随机盐值,另一部分是盐值和 secret 拼接后经过特定哈希计算的结果。然后 ctx.csrf 引用该 token,最后传送到窗体。secret 只会被创建一次,但是 token 会因每次使用的盐值不一样而不同(窗体刷新后 token 会改变,session 失效后 secret 会改变)。
  3. 当请求到来时,从 token 中提取盐值与 session.secret 计算结果,最后把计算结果与 token 作安全比较,决定请求是否为伪造。

CSRF 攻击者可以利用用户的会话凭据向服务器发送 session.secret,但不会知道它的具体值,所以没办法生成对应的 token,从而请求失败。

版权声明:

本文为博主原创文章,若需转载,须注明出处,添加原文链接。

https://leeguangxing.cn/blog_post_39.html