Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

【路由中间件】(弃用)防止csrf攻击

现在可以采用cookie的SameSite来防范此类攻击

  • 安装

    npm i -S koa-csrf
    
    • 最新版本 3.0.8
    npm i -S koa-session
    
    • 最新版本 6.2.0
    • koa-csrf 的机制要依赖 session 能力,否则下面的ctx.csrf会是null
    • 针对单服务器单进程可以用这个,但是还是建议更新为koa-generic-session,由redis全局管理session
  • 使用写法(全局添加,不推荐,会跟multer上传起冲突,除非自己加到action当中作为参数)

    import session from "koa-session";
    import CSRF from "koa-csrf";
    
    app.keys = [process.env.NEW_SESSION_KEY,process.env.OLD_SESSION_KEY];
    app.use(session(app))
    
    app.use(new CSRF({
      invalidTokenMessage: 'Invalid CSRF token',
      invalidTokenStatusCode: 403,
      excludedMethods: [ 'GET', 'HEAD', 'OPTIONS' ],
      disableQuery: false
    }));
    
    • 必须要提供密钥,这里要写到.env
      • 为什么要两个元素
        • 第一个是新的key,第二个是旧的key
        • 程序默认用第一个,然后这样可以无缝替换:
          • 当你想要把第一个key废弃时
          • 可以放到第二个旧key的位置
          • 然后设置个新的在第一个新key的位置
          • 这样不会让已签发的网页立刻失效
      • NEW_SESSION_KEY=xxx
      • OLD_SESSION_KEY=yyy
    • disableQuery
      • 设为false,表示允许通过URL字符串传递_csrf
      • 但这样可能会通过 Referer 泄露
      • 所以要尽量放到表单中,通过POST提交
  • 注意!!!

    • 这个中间件,如果全局的话必须要在 bodyParser后面 插入
    • 但最好不要加到全局,而是作为单独中间件,加到需要渲染添加_csrf值的页面路由中,然后用ctx.csrf引用
      import CSRF from "koa-csrf";
      
      router.get('/hello', new CSRF({
          invalidTokenMessage: 'Invalid CSRF token',
          invalidTokenStatusCode: 403,
          excludedMethods: [ 'GET', 'HEAD', 'OPTIONS' ],
          disableQuery: false
      }), async (ctx)=>{
          let title = 'hello koa2'
          console.log(ctx.csrf)
          await ctx.render('client/hello',{
              title,
              csrf: ctx.csrf
          })  
      })
      
      • 但session还是要加到全局
  • 然后在自己的ejs模板文件中

    • 给form添加一个隐藏的input,里面指定csrf值
      <form action="/api_v1/register" method="POST">
        <input type="hidden" name="_csrf" value="<%= csrf %>" />
        <input type="email" name="email" placeholder="Email" />
        <input type="password" name="password" placeholder="Password" />
        <button type="submit">Register</button>
      </form>
      
    • 渲染模板的时候,服务器要提供csrf字段,作为客户端提交的验证凭据
      await ctx.render('index', {
          title: 'EJS !',
          csrf: ctx.csrf
      });
      
  • 中间件会自动拦截所有POST请求中的_csrf字段,自动完成csrf值的校验

    • 如果通过,就按正常的路由逻辑走
    • 如果不通过,会返回前面指定的错误信息Invalid CSRF token,以及403错误
    • 所有的POST请求,都需要校验此字段,怎么办?
      • 可以取消全局中间件,只放到要求安全性的路由里
      • 也可以每次都手动body添加_csrf字段,这个字段由服务端渲染返回,赋值提交服务端
        • 但这样对于前后端分离的页面,怎么办???
  • 依次检查如下字段

    req.body._csrf
    req.query._csrf
    req.headers[‘csrf-token’]
    req.headers[‘xsrf-token’]
    req.headers[‘x-csrf-token’]
    req.headers[‘x-xsrf-token’]
    
  • 添加了session后,观察到cookie多了两项

    koa.sess=eyJzZWNyZXQiOiJfcEdUN3ExdVlVX0I2SldQRlZ3SG5vcWwiLCJfZXhwaXJlIjoxNjM5NzM3MTg2NDE3LCJfbWF4QWdlIjo4NjQwMDAwMH0=;
    koa.sess.sig=r-lkJKc7sbj0E2XoKZlE4J6nopo
    
    • 用来区分是哪一个用户和会话,便于取出此会话内对应的secret做csrf校验
      • 校验规则:
        • _csrf的前8位,与此secret合并,进行哈希,如果等于_csrf的后27位,则校验通过
      • 同一用户可以签发好多个_csrf,而名下只有一个secret
        • 所以就算打开新的页面,旧页面的_csrf值也不会失效
    • 当页面刷新了之后,因为是存在网站的cookie中,所以这两个值都不会变
      • 使得不受http协议无状态的影响
      • 可以精准确认浏览器内好几次请求,都是同一个人发出的