webSafe-csrf

web safe csrf

在 web 应用上存在很多安全风险,如 csrf(XSRF),既伪造用户请求向网站发起恶意请求,是一种对网站的恶意利用。

通常防御方式都是服务器分发凭证,每次提交请求或者表单时带上凭证给服务器校验是否为恶意的请求

常用的防范方案

  • 对于服务端渲染的表单页面可以把 token 渲染及隐藏在特定的地方,from 表单提交的时候把该 token 通过约定的参数带给后台
  • 将 token 设置在 Cookie 中,在提交 post 请求的时候提交 Cookie,并通过 header 或者 body 带上 Cookie 中的 token,服务端进行对比校验
  • 将 token 存在 custom header 上,服务端通过校验请求自定义头部字段值
  • 在链接 url 上带上 token 参数

Example

基于 koa 框架采用服务端 setCookie 的方式来讲解

步骤

  • 服务器接受到请求,通过响应页面时将 token 渲染到页面上的 form 隐藏域中<input type="hidden" name="_csrf" value="xxxx">
  • 服务端将 token 设置 cookie 带回客户端response headers -> Set-Cookie: xxx
  • 当用户发送 GET 或者 POST 请求时带上_csrf参数(对于 Form 表单直接提交即可,因为会自动把当前表单内所有的 input 提交给后台,包括_csrf
  • 后台在接受到请求后解析请求的 cookie 从 session 中 获取 secret 的值,然后和用户请求提交的 _csrf 做解析比较,如果相等表示请求是合法的

所需 middleware

koa-session

option

  • key,cookie key,默认 koa:sess
  • maxAge,存储时间,默认 1 小时
  • overwrite,覆盖同名的 cookie,默认允许
  • httpOnly,仅服务器可以访问 cookie,不予许客户端 js 访问,默认开启
  • signed,安全性相关,koa 的 cookie 本身带了安全机制的签名app.keys 密钥,通过ctx.cookies.set('name', 'tobi', { signed: true })就可以设置,对于 cookie 默认开启签名
  • rolling,强制在每个响应中设置会话标识符 cookie。 过期被重置为原始的 maxAge,重置到期倒计时。默认关闭。
  • renew,会话即将过期时更新会话。默认关闭
  • store,外部存储
  • encode,自定义编码
  • decode,自定义解码

koa-session 默认的 session 存储方式 cookie,同时也支持外部存储默认配置下,会使用 cookie 来存储 session 信息,也就是实现了一个”cookie session”。这种方式对服务端是比较轻松的,不需要额外记录任何 session 信息,但是也有不少限制,比如大小的限制以及安全性上的顾虑。用 cookie 保存时,实现上非常简单,就是对 session(包括过期时间)序列化后做一个简单的 base64 编码。其结果类似

koa:sess=eyJwYXNzcG9ydCI6eyJ1c2VyIjozMDM0MDg1MTQ4OTcwfSwiX2V4cGlyZSI6MTUxNzI3NDE0MTI5MiwiX21heEFnZSI6ODY0MDAwMDB9;

在实际项目中,会话相关信息往往需要再服务端持久化,因此一般都会使用外部存储来记录 session 信息。外部存储可以是任何的存储系统,可以是内存数据结构,也可以是本地的文件,也可以是远程的数据库。但是这不意味着我们不需要 cookie 了,由于 http 协议的无状态特性,我们依然需要通过 cookie 来获取 session 的标识(这里叫 externalKey )。koa-session 里的 external key 默认是一个时间戳加上一个随机串,因此 cookie 的内容类似

koa:sess=1517188075739-wnRru1LrIv0UFDODDKo8trbmFubnVmMU

koa-csrf

原理:

koa-csrf 会在 session 中保存一个secret字段,创建一个新的密钥。

1
ctx.session.secret = ** (Create a new secret key synchronously)

使用密钥生成 token,由于每次请求都是重新生成 salt,因此每次 token 都不一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# secret是上面生成的密钥
# salt是salt是随机生成的字符串,长度可自定
ctx.csrf = token = salt + '-' + hash(salt + '-' + secret)

# hash函数源码
function hash (str) {
return crypto
.createHash('sha1')
.update(str, 'ascii')
.digest('base64')
.replace(PLUS_GLOBAL_REGEXP, '-')
.replace(SLASH_GLOBAL_REGEXP, '_')
.replace(EQUAL_GLOBAL_REGEXP, '')
}

验证的时候,只需要取出 token 头部的 salt,再从 session 中取出 secret,再生成 expected,与 token 对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 校验函数
function verify (secret, token) {
if (!secret || typeof secret !== 'string') {
return false
}

if (!token || typeof token !== 'string') {
return false
}

var index = token.indexOf('-')

if (index === -1) {
return false
}

var salt = token.substr(0, index)
var expected = this._tokenize(secret, salt) # _tokenize函数为生成token

return compare(token, expected) # 判断是否一致
}

添加中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
...
# 添加session会发机制中间件
this.app.use(
session(
{
key: config.cookie, // cookie key
maxAge: 86400000, // 存储时间,默认1小时
overwrite: true, // 覆盖同名的cookie
httpOnly: true, // 仅服务器可以访问cookie,不予许客户端js访问
signed: true, // 安全性,签名
rolling: false,
renew: false
},
this.app
)
);
# 添加CSRF中间件
this.app.use(
new CSRF({
invalidSessionSecretMessage: 'Invalid session secret',
invalidSessionSecretStatusCode: 403,
invalidTokenMessage: 'Invalid CSRF token',
invalidTokenStatusCode: 403,
excludedMethods: ['GET', 'HEAD', 'OPTIONS'],
disableQuery: false
})
);
...

创建 ejs 模板,其中_csrf认证的字段

1
2
3
4
5
6
<form action="/csrf/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>

ctx.csrf带有生成的 token

1
2
3
4
5
6
7
8
async index(ctx: any) {
if (ctx.method === 'GET') {
await ctx.render('csrf', {
title: 'web safe csrf',
csrf: ctx.csrf
});
}
}

第一次访问的时候服务器会响应 setCookie,后面请求都会

提交时候把_csrf字段带给服务器校验

项目 demo 地址,yarn run csrf

相关链接

koa-session 基础知识(写的还是不错)
聊聊 CSRF(另一种实现方式)