[Web 安全] 如何通过JWT防御CSRF

标签: web安全 csrf jwt | 发表时间:2015-09-07 14:33 | 作者:名一
出处:http://segmentfault.com/blogs

名字都是用来唬人的。

先解释两个名词,CSRF 和 JWT。

CSRF (Cross Site Request Forgery),它讲的是你在一个浏览器中打开了两个标签页,其中一个页面通过窃取另一个页面的 cookie 来发送伪造的请求,因为 cookie 是随着请求自动发送到服务端的。

JWT (JSON Web Token),通过某种算法将两个 JSON 对象加密成一个字符串,该字符串能代表唯一用户。

CSRF 的产生

首先通过一个图来理解 CSRF 是什么现象。

CSRF

想要攻击成功,这三步缺一不可。

  • 第一,登录受害者网站。如果受害者网站是基于 cookie 的用户验证机制,那么当用户登录成功后,浏览器就会保存一份服务端的 SESSIONID。

  • 第二,这时候在同一个浏览器打开攻击者网站,虽然说它无法获取 SESSIONID 是什么(因为设置了 http only 的 cookie 是无法被 JavaScript 获取的),但是从浏览器向受害者网站发出的任何请求中,都会携带它的 cookie,无论是从哪个网站发出。

  • 第三,利用这个原理,在攻击者网站发出一个请求,命令受害者网站进行一些敏感操作。由于此时发出的请求是处于 session 中的,所以只要该用户有权限,那么任何请求都会被执行。

比如,打开优酷,并登录。再打开攻击者网站,它里面有个 <img> 标签是这样的:

  <img src="http://api.youku.com/follow/123" />

这个 api 只是个例子,具体的 url 和参数都可以通过浏览器的开发者工具(Network 功能)事先确定。假如它的作用是让该登录的用户关注由 123 确定的一个节目或者用户,那么通过 CSRF 攻击,这个节目的关注量就会不断上升。

解释两点。第一,为什么举这个例子,而不是银行这种和金钱有关的操作?很简单,因为它容易猜。对于攻击者来说,没有什么是一定能成功的,比如 SQL 注入,攻击者他不知道某网站的数据库是怎么设计的,但是他一般会通过个人经验去尝试,比如很多网站把用户的主键设置为 user_id,或 sys_id 等。

银行的操作往往经过多重确认,比如图形验证码、手机验证码等,光靠 CSRF 完成一次攻击基本上是天方夜谭。但其他类型的网站往往不会刻意去防范这些问题。虽然金钱上的利益很难得到,但 CSRF 能办到的事情还是很多,比如利用别人发虚假微博、加好友等,这些都能对攻击者产生利益。

第二,如何确保用户打开优酷之后,又打开攻击者网站?做不到。否则任何人打开优酷之后,都会莫名其妙地去关注某个节目了。但是你要知道,这个攻击成本仅仅是一条 API 调用而已,它在哪里都能出现,你从任何地方下载一张图片,让你请求这个地址,看也不看就点确定,请求不就发出去了吗?

CSRF 的防御

对于如何防范 CSRF,一般有三种手段。

判断请求头中的 Referer

这个字段记录的是请求的来源。比如 http://www.example.com 上调用了百度的接口 http://api.map.baidu.com/service 那么在百度的服务端,就可以通过 Referer 判断这个请求是来自哪里。

在实际应用中,这些跟业务逻辑无关的操作往往会放在拦截器中(或者说过滤器,不同技术使用的名词可能不同)。意思是说,在进入到业务逻辑之前,就应该要根据 Referer 的值来决定这个请求能不能处理。

在 Java Servlet 中可以用 Filter(古老的技术);用 Spring 的话可以建拦截器;在 Express 中是叫中间件,通过 request.get('referer') 来取得这个值。每种技术它走的流程其实都一样。

但要注意的是,Referer 是浏览器设置的,在浏览器兼容性大不相同的时代中,如果存在某种浏览器允许用户修改这个值,那么 CSRF 漏洞依然存在。

在请求参数中加入 csrf token

讨论 GET 和 POST 两种请求,对于 GET,其实也没什么需要防范的。为什么?因为 GET 在“约定”当中,被认为是查询操作,查询的意思就是,你查一次,查两次,无数次,结果都不会改变(用户得到的数据可能会变),这不会对数据库造成任何影响,所以不需要加其他额外的参数。

所以这里要提醒各位的是,尽量遵从这些约定,不要在 GET 请求中出现 /delete, /update, /edit 这种单词。把“写”操作放到 POST 中。

对于 POST,服务端在创建表单的时候可以加一个隐藏字段,也是通过某种加密算法得到的。在处理请求时,验证这个字段是否合法,如果合法就继续处理,否则就认为是恶意操作。

  <form method="post" action="/delete">
  <!-- 其他字段 -->
  <input type="hidden" name="csrftoken" value="由服务端生成"/>
</form>

这个 html 片段由服务端生成,比如 JSP,PHP 等,对于 Node.js 的话可以是 Jade 。

这的确是一个很好的防范措施,再增加一些处理的话,还能防止表单重复提交。

可是对于一些新兴网站,很多都采用了“单页”的设计,或者退一步,无论是不是单页,它的 HTML 可能是由 JavaScript 拼接而成,并且表单也都是异步提交。所以这个办法有它的应用场景,也有局限性。

新增 HTTP Header

思想是,将 token 放在请求头中,服务端可以像获取 Referer 一样获取这个请求头,不同的是,这个 token 是由服务端生成的,所以攻击者他没办法猜。

这篇文章的另一个重点——JWT——就是基于这个方式。抛开 JWT 不谈,它的工作原理是这样的:

JWT

解释一下这四个请求,类型都是 POST 。

  1. 通过 /login 接口,用户登录,服务端传回一个 access_token,前端把它保存起来,可以是内存当中,如果你希望用来模拟 session 的话。也可以保存到 localStorage 中,这样可以实现自动登录。

  2. 调用 /delete 接口,参数是某样商品的 id。仔细看,在这个请求中,多了一个名为 Authoriaztion 的 header,它的值是之前从服务端传回来的 access_token,在前面加了一个“Bearer”(这是和服务端的约定,约定就是说,说好了加就一起加,不加就都不加……)

  3. 调用 /logout 接口,同样把 access_token 加在 header 中传过去。成功之后,服务端和前端都会把这个 token 置为失效,或直接删除。

  4. 再调用 /delete 接口,由于此时已经没有 access_token 了,所以服务端判断该请求没权限,返回 401 。

各位有没有发现,从头至尾,整个过程没有涉及 cookie,所以 CSRF 是不可能发生的!

关于 JWT 的约定

如果不关心 JWT,那文章完全可以结束了,因为看到这里,除了章节标题提到的内容之外,各位还可以引申出几点:第一,在设计 API 时多斟酌一下;第二,利用 token 做单点登录;第三,cookie 和 token 这两种用户验证机制的不同。

而 JWT,其实就是对新增的 HTTP Header 的约定。就比如 GET 请求中的参数,约定了用 & 分隔,但是用别的可以吗?当然可以,你用 逗号 或者 分号 也行啊,服务端再规定一个转义的规则就行了。只不过,约定是为了让所有人更规范地做事情,如果按照约定行事的话,那从一个工具换到另一个工具,自己需要改的代码就很少。这里就不深入谈了。

三个组成部分

这个网站 对 JWT 的术语和内容有最官方的说明。

JWT 的每个部分都是字符串,由 点 分隔,所以它的格式是这样的:

  XXX1.XXX2.XXX3

整个字符串是 URL-safe 的,所以可以直接用在 GET 请求的参数中。

第一部分 JWT Header

它是一个 JSON 对象,表示这个整个字符串的类型和加密算法,比如

  {
  "typ":"JWT",
  "alg":"HS256"
}

经过 base64url 加密之后变成

  eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9

第二部分 JWT Claims Set

它也是一个 JSON 对象,能唯一表示一个用户,比如

  {
  "iss": "123",
  "exp": 1441593850
}

经过 base64url 加密之后变成

  eyJpc3MiOiIxMjMiLCJleHAiOjE0NDE1OTM4NTB9

在官网有详细的属性说明,尽量使用里面提到的 Registered Claim Names,这样可以提高阅读性。这里的 iss 表示 issuer,就是发起请求的人,它的值是跟业务相关的,所以由你的应用去决定。exp 表示 expiration time,即什么时候过期,注意,这个值是秒数,而不是毫秒数,所以是在整型范围内的。

第三部分 JWS Signature

这个签名的计算跟第一部分中的 alg 属性有关,假如是 HS256,那么服务端需要保存一个私钥,比如 secret 。然后,把第一部分和第二部分生成的两个字符串用 点 连接之后,再结合私钥,用 HS256 加密可以得到如下字符串

  AOtbon6CebgO4WO9iJ4r6ASUl1pACYUetSIww-GQ72w

现在就集齐三个部分了,用 . 连接,得到完整的 token 。

例子 1/2:以 Express 作为服务端

对于服务端来说,已经存在各种库去支持 JWT 了,推荐几个如下:

平台
Java maven com.auth0 / java-jwt / 0.4
PHP composer require lcobucci/jwt
Ruby gem install jwt
.NET Install-Package System.IdentityModel.Tokens.Jwt
Node.js npm install jsonwebtoken

如果之前有 Node.js 和 Express 的学习经历的话,那对下面的代码应该很容易理解。

  var express = require('express'),
    jwt     = require('jsonwebtoken');

var router      = express.Router(),
    PRIVATE_KEY = 'secret';

router.post('/login', function(req, res, next) {

    // 生成 JWT
    var token = jwt.sign({
        iss: '123'
    }, PRIVATE_KEY, {
        expiresInMinutes: 60
    });

    // 将 JWT 返回给前端
    res.send({
      access_token: token
    });
});

router.post('/delete', function(req, res, next) {
    var auth    = req.get('Authorization'),
        token   = null;

    // 判断请求头中是否有 Authoriaztion 字段,为了缩短代码就减少了别的验证
    if (auth) {
        token = /Bearer (.+)/.exec(auth)[1];
        res.send(jwt.decode(token));
    } else {
        res.sendStatus(401);
    }
});

关于 jsonwebtoken 的使用可以看它的 手册

例子中定义了两个 API。

  • /login,会返回一个 JWT 字符串。其中包含了一个用户 id,和存活时间,这个时间会被转换成 exp 和 iat (issue at, 发起请求的时间),两者之差就是存活时间。

  • /delete,验证请求头中是否有 Authorization 字段,并且是否合法,如果是的话就处理请求,否则返回 401 。

注意一下,服务端期待的 Authoriaztion 请求头是这样的格式:

  Authorization: Bearer XXX1.XXX2.XXX3

这个跟 JWT 无关,是 OAuth 2.0 的一种格式。因为 Authorization 这个字段也是约定的,它由 token 的类型和值组成,类型除了上文提到的 Bearer,还有 Basic、MAC 等。

例子 2/2:以 Backbone 作为前端

前端的工作分两方面,一是存储 jwt,二是在所有的请求头中增加 Authoriaztion 。

如果是重构已有的代码,第二个工作可能有点难度,除非旧代码中的表单都是异步提交,并且请求的方法是自己包装过的,因为只有这样才有机会去修改请求头。

在若干星期之前的 这篇文章中,写了怎么在 Angular 中拦截请求。现在就以 Backbone 为例。

  // 先保存原始的 sync 方法
var sync = Backbone.sync;

Backbone.sync = function (method, model, options) {

  var token = window.localStorage.getItem('jwt');

  // 如果存在 token,就把它加到请求头中
  if (token) {
    options.headers.Authorization = 'Bearer ' + token;
  }

  // 调用原始的 sync 方法
  sync(method, model, options);
};

对跨域的额外处理

在跨域的应用场景中,需要服务端做一些额外的设置,这些设置是加在响应头上的。

  Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Authorization

第一个表示允许来自任何域名的请求。第二个表示允许一些 自定义的请求头,因为 Authoriaztion 是自定义的,所以必须加上这个配置,如果各位使用了其他的请求头,请同样加上。

如果服务端用了 nginx,那么这些配置可以写在 nginx.conf 文件中。如果是在代码中配置,那么无论是 Java,还是 Node.js,都有 response.setHeader 方法。

小结

我对 Web 安全方面的了解还不太深,所以没有太多经验可谈。安全性是一个在平常不太受重视的领域,因为完成一个项目的优先级从来都是:功能 > 颜值 > 性能, 安全 。至少得保证用户在使用过程中不会出错,然后再做得酷炫或清新一点,性能和安全只有在满足了前两项,或者迫在眉睫的时候才去考虑。当服务器承受不了那么高的负载了,才会去增加更多的服务器,但业务功能从一开始就不能少。

可是这样做有错吗?并没有吧。在特定的场景,做特定的处理,或许是性价比最高的决策了。

这篇文章中反复提到的一个词是“ 约定”,它貌似和“ 具体情况具体分析”这个观点矛盾了,额……。

约定是人与人之间的共识,比如说 GET 请求,那么对方的第一反应就是查询,当有人破坏约定,用 GET 请求去做删除操作时,就会让别人很难理解(当有一大堆人这么做的时候,就不难理解了吧……)。或者当我们提到 JWT 的时候,那它就应该是由三个部分组成,如果有人仅仅是按照自己的算法来生成一个 token,同样可以唯一标识用户,那他必须得像共事的人解释,这个算法的安全性、使用方法等。

另一方面,如果真心觉得按照“约定”办事没必要,太麻烦,并且可以接受“耍小聪明”的后果的话,那就按自己的想法去做吧(真的不再考虑一下了吗)。

为什么 HTML5 新增了那么多语义化的标签,是因为一切都在朝着更规范的方向走。

相关 [web 安全 jwt] 推荐:

[Web 安全] 如何通过JWT防御CSRF

- - SegmentFault 最新的文章
先解释两个名词,CSRF 和 JWT. CSRF (Cross Site Request Forgery),它讲的是你在一个浏览器中打开了两个标签页,其中一个页面通过窃取另一个页面的 cookie 来发送伪造的请求,因为 cookie 是随着请求自动发送到服务端的. JWT (JSON Web Token),通过某种算法将两个 JSON 对象加密成一个字符串,该字符串能代表唯一用户.

什么是 JWT -- JSON WEB TOKEN - 简书

- -
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(. (RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景. JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密.

JWT Tool:针对 JSON Web Tokens 的测试工具

- - FreeBuf互联网安全新媒体平台
众望所归,大家期待已久的JWT渗透测试工具终于出炉啦. 没错,今天给大家介绍的这款名叫JWT Tool的工具,就可以针对JSON Web Tokens进行渗透测试. JWT是JSON Web Token的缩写,它是一串带有声明信息的字符串,由服务端使用加密算法对信息签名,以保证其完整性和不可伪造性.

安全隐患名录-Web

- - Onlycjeg's Blog
[0day储藏室出品]安全隐患名录-Web. 本文档是基于OWASP TOP 10写成,描述了OWASP TOP 10中所讲到的风险并对其进行风险等级鉴定,漏洞描述,漏洞危害,测试方法,测试过程对系统的影响以及修复方法. 在做项目的过程中,我们所面对的更多的是生产系统,并不是所有的企业都存在测试服务器.

Web开发框架安全杂谈

- goodman - 80sec
最近框架漏洞频发,struts任意代码执行、Django csrf token防御绕过、Cakephp代码执行等等各大语言编程框架都相继暴出高危漏洞,这说明对于编程框架的安全问题已经逐渐走入安全工作者的视线. Web开发框架就相当于web应用程序的操作系统,他决定了一个应用程序的模型结构和编程风格.

[转][转]浅谈php web安全

- - heiyeluren的Blog
来源: http://www.phpben.com/?post=79. 首先,笔记不是web安全的专家,所以这不是web安全方面专家级文章,而是学习笔记、细心总结文章,里面有些是我们phper不易发现或者说不重视的东西. 在大公司肯定有专门的web安全测试员,安全方面不是phper考虑的范围. 但是作为一个phper对于安全知识是:“ 知道有这么一回事,编程时自然有所注意”.

华为内部的Web安全原则

- - 服务器运维与网站架构|Linux运维|X研究
Web安全原则 1.认证模块必须采用防暴力破解机制,例如:验证码或者多次连续尝试登录失败后锁定帐号或IP. 说明:如采用多次连续尝试登录失败后锁定帐号或IP的方式,需支持连续登录失败锁定策略的“允许连续失败的次数”可配置,支持在锁定时间超时后自动解锁. 2.对于每一个需要授权访问的页面或servlet的请求都必须核实用户的会话标识是否合法、用户是否被授权执行这个操作,以防止URL越权.

Web安全扫描器Netsparker v3.5发布

- - FreeBuf.COM
‍‍Netsparker是一款综合型的web应用安全漏洞扫描工具,它分为专业版和免费版,免费版的功能也比较强大. Netsparker与其他综合 性的web应用安全扫描工具相比的一个特点是它能够更好的检测SQL Injection和 Cross-site Scripting类型的安全漏洞. * DOM跨站脚本漏洞检测 * 基于Chrome浏览器的Dom解析 * URL重写规则 * 可晒出某些不必要的扫描结果.

从“黑掉Github”学Web安全开发

- - 酷 壳 - CoolShell.cn
Egor Homakov(Twitter:  @homakov 个人网站:  EgorHomakov.com)是一个Web安全的布道士,他这两天把github给黑了,并给github报了5个安全方面的bug,他在他的这篇blog——《 How I hacked Github again》(墙)说明了这5个安全bug以及他把github黑掉的思路.

零基础如何学习 Web 安全?

- - 知乎每日精选
这是个好问题,我强迫症犯了,本来你写的是“web”,我改为了“Web”. 因为正好Web安全是我擅长的,你说的是 0基础,我总结下我的一些看法吧,针对0基础的. Web分为好几层,一图胜千言:. 事实是这样的: 如果你不了解这些研究对象是不可能搞好安全研究的. 这样看来,Web有八层(如果把浏览器也算进去,就九层啦,九阳神功……).