从前端安全出发理解OAuth2.0

2,947 阅读11分钟

OAuth2.0是一套非常经典、流行的授权框架,但是我们在学习和使用它的过程中会觉得它的授权认证流程非常繁琐,造成学习和使用OAuth2.0的时间成本很高。所以今天我将从前端安全的角度出发,使用一些针对授权流程的攻击手段和策略通过攻防演练的方式向你展示OAuth2.0是如何通过复杂的授权流程与巧妙的设计来防御攻击者的攻击。

简要介绍OAuth 2.0

​ OAuth2.0是一套授权框架,它定义了 用户 通过 授权服务器第三方应用 进行授权的过程。提供了让用户无需在第三方应用上输入账号密码即可安全的对第三方应用进行授权的能力。OAuth2.0针对应用所处的运行环境制定了四种授权模式,今天我们主要讨论其中认证流程最完整、严密的授权码模式。

为了方便和专注于我们今天所讨论的主题,所以我将OAuth2.0框架所定义的角色和流程缩减为下图的三个角色、四步流程。(需要格外关注的是除第一步用户授权流程是 第三方应用的客户端授权服务器 进行通信,其他流程均在 第三方应用的服务端授权服务器 间通信完成)

攻击策略一:中间人攻击

​ 中间人攻击的主要原理是攻击者通过伪装将自己置于两个端的通信网络中,形成一个“中转站”,其主要攻击方式是对通信信息的窃取与篡改。而针对一般授权流程(如非OAuth2.0)的中间人攻击,攻击者往往通过劫持或伪造基站、网卡、wifi等通信服务窃取客户端与服务端中明文传输的令牌来获取用户所授权的信息与权利。

通过简单的介绍我们可以了解到以下几个中间人攻击的特征:

  1. 攻击者主要针对通讯网络且都是他能够渗透到的通信网络,且是明文传输的。
  2. 攻击者要伪装自己。反过来说,不能让被攻击者警觉。

在了解了中间人攻击的主要过程和特征后,我们就可以对症下药,看看OAuth2.0是如何防御住中间人攻击的:

在整个OAuth2.0授权认证流程中,绝大部分的通信发生在第三方应用的服务端与授权服务器之间这就可以保证这部分通信是不会被攻击者窃取的,但是还有一部分通信流程是可能被暴露在攻击者眼前的,就如第三方应用的客户端与服务端之间的通信可能就被用户手机自动连接上的免费wifi所监听着。OAuth2.0对此的防御策略就是所有需要加密的、可靠的信息都由服务端之间的通信进行交换比如令牌、密钥和用户信息等。而可能会遭受中间人攻击的客户端与服务端的通信则默认其通信信息已经被窃取或篡改,故而只让它交换公开的、不可靠的信息,如公钥和授权码。当然针对中间人攻击最有效的解决办法还是通过HTTPS协议将通信信息加密,但是OAuth2.0的出现正是解决了针对HTTP协议安全授权的问题,所以不得不讨论最坏的情况。

以上我们已经充分利用中间人攻击的第一个特征进行防御,但是毕竟终归有暴露在攻击者眼前的通信信息:授权码。如何保证攻击者不会拿到授权码后进一步骗取授权服务器对其颁发令牌呢?这就要开始解释为什么上文提到授权码是不可靠的信息,首先介绍授权码的两个特点:

  1. 时效短,通常只有五分钟有效期
  2. 只能使用一次

针对授权码的两个特点我们想象一下这样两个攻击场景,攻击者因为是中间人所以必定比客户端要提前拿到授权码,如果攻击者没有使用该授权码直接将其转给了客户端,客户端使用后由于授权码只能使用一次的特点攻击者必然无法重复使用因此攻击失败。如果攻击者率先使用了授权码,并且将使用后的授权码或伪造的授权码传递给客户端,那么客户端必然会授权失败从而引起用户的警觉。当然,光警觉不够,毕竟攻击者成功拿到了授权码,那么是什么机制拦截住了攻击者用授权码换取令牌的动作呢?就是上文我们提到的,也是我们下文经常要提的密钥(secret)。

应用ID与应用密钥

​ 在介绍接下来的攻击策略前,我需要先简单介绍下应用ID(client_id)和应用密钥(client_secret)的来历和用途。在OAuth2.0中规定第三方应用在接入授权服务前要先在授权服务方注册应用,并提供应用授权回调页(图1中的第4步)等信息。注册完成后授权服务方会给第三方应用两个信息应用ID和应用密钥,应用ID是识别应用的身份码,可公开比如写死在客户端代码中,应用密钥是确认应用身份的信息,需保密只能在服务端中使用。用一句话概括就是:应用ID告诉授权服务器“我”是谁,而应用密钥则是让授权服务器相信“我”是“我”。

攻击策略二:CSRF绑定劫持

​ 我们知道CSRF的主要攻击过程是构造恶意链接,诱骗用户点击。而在一般的授权过程中攻击者往往使用一种叫做绑定劫持的攻击策略,将攻击者的账号与被攻击者的账号绑定在一起。那么如何理解绑定劫持呢?举个例子,我们玩游戏通常最痛恨盗号狗,因为盗号者通过某些方法登录上了我们的游戏账号给我们造成了损失。而绑定劫持则恰恰相反,绑定劫持指的是攻击者诱骗被攻击者登录了攻击者的账号,比如被攻击者的游戏账号被诱骗登录并绑定了攻击者的账号,那么攻击者便可以顺理成章的登录上被攻击者的游戏账号了。

​ 在OAuth2.0授权流程中这样的恶意链接极易构建:只需要攻击者正常授权第三方应用,但将回调地址故意填错或伪造,授权服务器就会带着攻击者的授权码重定向到这个错误的地址,而这个授权码自然也就未曾被使用过。此时攻击者再将这个错误的地址改为正确的回调地址,并将这个链接发送给被攻击者,诱骗其点击。被攻击者就自然而然的进行了授权码换令牌,令牌换…的过程,这一套授权认证流程经过后就成功登录上了被攻击者的账号落入了攻击者的圈套。

​ OAuth2.0防御CSRF绑定劫持的方法非常巧妙。它通过设置一个字段state其值为hash。在第三方应用获取授权码时将其传递给授权服务器,在授权服务器带着授权码重定向到第三方应用服务端时再将state原封不动传回,第三方应用服务端会校验前后两次state是否相同。相同则证明授权者与登录者是同一人,不相同则拒绝该登录请求防止CSRF绑定劫持。值得思考的是state字段在OAuth2.0中是作为一个选填字段使用的,我猜想可能这种登录上攻击者账号的攻击方式对被攻击者的风险更小吧。

攻击策略三:CSRF伪造重定向地址

​ 这也是一个基于CSRF的攻击策略,只不过它针对的攻击对象是类似OAuth2.0这种带有重定向操作的授权认证流程。它的攻击方式是通过构造虚假回调地址,利用授权服务对回调地址检查不严的漏洞,将被攻击者引入事先构造好的网页,窃取其授权信息。

​ 值得注意的是,文中提到的“事先构造好的网页”不单指攻击者搭建的网站,也可能是第三方应用中某些用户可以输入内容的网页,经过攻击者构造后便可以窃取授权认证信息。举个例子:假设第三方应用上有个网页支持用户评论、支持用户插入链接图片且没有将用户插入的链接地址洗成第三方应用的链接地址。那么攻击者便可以将个人服务器上的图片资源链接地址上传至该网页评论处,然后构造授权链接并将回调地址填写成该评论网页,此时可能由于该网页与第三方应用的授权页同属于相同的一级域名就绕过了检查。此时授权服务器带着授权信息重定向到这个评论网页,在查询字符串中的所有授权信息便会都在referer字段中一并暴露给攻击者。

这个问题其实不算严重,毕竟上文中间人攻击那里也说过了回调页拿到的授权码信息并不可靠,而且还有应用密钥的存在,使得授权码无法正常换取令牌。但是毕竟是重定向到了攻击者构造的页面上,若是配合其他的攻击手段可能问题会很严重,所以根治CSRF伪造重定向地址的方法就是严格校验回调地址。以下的几个回调地址就是攻击者构造的恶意地址的典型。

example.com/auth/callba…

example.com/redirect.ph…

​ 按照OAuth2.0最严格的回调地址校验规则来说,应用在授权服务方注册的时候就应该已经规定好了重定向地址的路由,且不可带参数,要全等于。不过安全向来都是相对的,若是单纯的内部系统使用的授权服务,通过一下代码取到hostname后和注册应用里的hostname比较下是否全等于也就足够了:

const { hostname } = new URL(redirectURI)

攻击策略四:DNS污染

​ DNS污染的目的其实CSRF伪造重定向地址是相同的,都是想让被攻击者重定向到攻击者构造到网页,不同的是DNS污染成本更高,攻击范围更广。我们知道DNS是为浏览器提供域名到IP地址映射信息服务的分布式数据库,当下游DNS服务找不到某域名与IP地址的映射时会往上游进行请求查询。找到映射后为了下次查询的快速相应,下游DNS服务会缓存之前查询的结果,而DNS污染就是攻击者通过某些手段改写了DNS服务中的缓存。由此可想象的到,即便是授权服务器做足了回调地址的检查工作也难免会让第三方应用在查询受污染的DNS服务后跳到攻击者恶意构造的网站中。到了这一步也就只有上文提到的应用密钥可以保证攻击者拿不到令牌和用户信息。

总结

​ 以上展示的就是OAuth2.0防御常见的针对授权流程攻击的设计实现。在OAuth2.0设计理念中我认为最关键的核心就是 公开的信息短效且不信任、保密的信息只通过服务端传递和保存

​ 复杂逻辑 + 弱约束 = 容易滋生漏洞

​ 因为OAuth2.0流程非常复杂并且其标准对开发者的技术实现和细节约束很弱,所以在我们开发者实现或接入OAtuh2.0 过程中一定要明白OAuth2.0设计的原理,就比如当我们明白了保密信息只能通过服务端传递和保存后就不会做出将令牌存放在Cookies这样的操作。

​ 最后,OAuth2.0中还有很多细节我们没有去讨论,比如token的生成需要回调地址的参与、作用域(scope)的安全作用。但是我们要明白OAuth2.0对授权流程的设定绝非一个字段防御一种攻击这么简单,而是通过多个字段、流程的配合编织成一张致密的网络来完成防御功能。同样的,攻击者的攻击手段也往往不是我们演示的这么简单、单一,往往攻击者会针对开发者功能实现上的漏洞打出一整套组合拳。所以对于我们开发者来说明白核心原理才能在技术细节上少出纰漏毕竟永远没有最安全,只有更安全。