
大家好! 我是Mail.Ru Mail的信息安全专员Nikita Stupin。 不久前,我对移动OAuth 2.0进行了漏洞研究。 要创建安全的移动OAuth 2.0方案,仅以纯形式实现标准并检查redirect_uri是不够的。 有必要考虑到移动应用程序的细节并应用其他保护机制。
在本文中,我想与您分享有关对移动OAuth 2.0的攻击,保护方法和该协议的安全实现的知识。 我将在下面讨论的所有必要的保护组件都已在Mail.Ru Mail移动客户端的最新SDK中实现。
OAuth 2.0的性质和功能
OAuth 2.0是一种授权协议,描述了客户端服务访问服务提供商上用户资源的安全性。 同时,OAuth 2.0使得用户不必在服务提供商外部输入密码:整个过程简化为单击“我同意授予...的访问权限”按钮。
就OAuth 2.0而言,提供程序是一种拥有用户数据的服务,并且在用户的许可下,向第三方服务(客户端)提供对此数据的安全访问。 客户端是想要从提供者接收用户数据的应用程序。
OAuth 2.0协议发布后的一段时间,普通开发人员对其进行了修改以进行身份验证,尽管它最初并非旨在用于此目的。 身份验证将攻击向量从存储在服务提供商处的用户数据转移到用户服务用户帐户。
它不仅限于认证。 在移动应用程序和转换的崇高时代,仅需一个按钮即可进入应用程序。 开发人员将OAuth 2.0放在了移动轨道上。 自然,很少有人考虑过移动应用程序的安全性和特性:一次又一次地在生产中。 但是,OAuth 2.0通常无法在Web应用程序之外正常运行:在移动和桌面应用程序中都观察到相同的问题。
让我们弄清楚如何制作安全的移动OAuth 2.0。
如何运作?
请记住,在移动设备上,客户端可能不是浏览器,而是没有后端的移动应用程序。 因此,我们面临移动OAuth 2.0的两个主要安全问题:
- 客户端不受信任。
- 从浏览器重定向到移动应用程序的行为取决于用户安装的设置和应用程序。
移动应用程序是公共客户端
为了了解第一个问题的根源,让我们看看在服务器到服务器交互的情况下OAuth 2.0的工作原理,然后在客户端到服务器交互的情况下将其与OAuth 2.0进行比较。
在这两种情况下,一切都始于服务客户端向服务提供者注册并接收
client_id
以及在某些情况下为
client_secret
的事实。
client_id
值为public,是标识客户端服务所必需的,与
client_secret
的值为private不同。 注册过程在
RFC 7591中有更详细的描述。
下图显示了OAuth 2.0在服务器到服务器通信中的操作。
图片取自https://tools.ietf.org/html/rfc6749#section-1.2OAuth 2.0协议分为3个主要阶段:
- [AC步骤]获取授权码(以下简称
code
)。 - [DE步骤]交换
access_token
code
。 - 使用
access_token
访问资源。
让我们更详细地检查代码的接收:
- [步骤A]服务客户端将用户重定向到服务提供商。
- [步骤B]服务提供商向用户请求允许向客户端服务提供数据的权限(箭头B向上)。 用户可以访问数据(右侧的箭头B)。
- [步骤C]服务提供商将
code
返回到用户的浏览器,该浏览器将code
重定向code
客户端服务。
让我们
access_token
更详细的获取
access_token
信息:
- [步骤D]客户端服务器发送一个
access_token
请求。 该请求包括: code
, client_secret
和redirect_uri
。 - [步骤E]在有效
code
client_secret
和redirect_uri
的情况下,提供client_secret
。
对
access_token
的请求是根据服务器到服务器的方案执行的,因此,通常,为了窃取
client_secret
攻击者必须入侵服务器-客户端-服务器或服务提供商的服务器。
现在,让我们看看在没有后端(客户端到服务器交互)的移动设备上OAuth 2.0方案的外观。
图片取自https://tools.ietf.org/html/rfc8252#section-4.1总体方案分为相同的3个主要步骤:
- [图片中的步骤1-4]获取
code
。 - [图片中的步骤5-6]交换
access_token
code
。 - 使用
access_token
访问资源。
但是,在这种情况下,移动应用程序还充当服务器,这意味着
client_secret
将被
client_secret
到应用程序内部。 这导致以下事实:在移动设备上,不可能对攻击者保密
lient_secret
。
client_secret
两种方法可以将
client_secret
到应用程序中:过滤从应用程序到服务器的流量或对应用程序进行反向工程。 两种方法都易于实现,因此
client_secret
在移动设备上毫无用处。
关于客户端到服务器方案,您可能会有一个问题:“为什么不立即获得
access_token
?”。 看来,为什么我们需要额外的步骤? 此外,存在一个
隐式授予方案,其中客户端立即收到
access_token
。 尽管可以在某些情况下使用它,但下面我们将看到
隐式授予不适用于安全的移动OAuth 2.0。
在移动设备上重定向
通常,要从浏览器重定向到移动设备上的应用程序,将使用“
自定义URI方案”和“
AppLink”机制。 这些机制的纯形式都不像浏览器重定向那样可靠。
自定义URI方案(或深层链接)的用法如下:开发人员在组装之前定义应用程序方案。 该方案可以是任意的,而在同一设备上可以安装具有相同方案的多个应用程序。 当设备上的每个应用程序都对应一个应用程序时,一切都非常简单。 但是,如果两个应用程序在同一设备上注册了相同的电路怎么办? 访问自定义URI方案时,操作系统如何确定要打开两个应用程序中的哪个? Android将显示一个窗口,其中包含您要在其中打开链接的应用程序的选择。 在iOS中,
未定义行为 ,这意味着可以打开两个应用程序中的任何一个。 在这两种情况下,攻击者
都可以拦截代码或access_token 。
与自定义URI方案相比,AppLink可以确保打开正确的应用程序,但是此机制有几个缺点:
- 每个服务客户必须独立通过验证程序 。
- Android用户可以在设置中关闭特定应用程序的AppLink。
- 6.0以下的Android和9.0以下的iOS不支持AppLink。
AppLink的上述缺点首先会增加潜在客户服务的进入门槛,其次会导致在某些情况下用户将无法使用OAuth 2.0。 这使得AppLink不适合替换OAuth 2.0协议中的浏览器重定向。
好吧,要攻击什么?
移动OAuth 2.0的问题也引起了特定的攻击。 让我们看看它们是什么以及它们如何工作。
授权码拦截攻击
初始数据:在用户设备上安装了合法应用程序(OAuth 2.0客户端)和注册了与合法方案相同方案的恶意应用程序。 下图显示了攻击方案。
图片取自https://tools.ietf.org/html/rfc7636#section-1这就是问题所在:在第4步中,浏览器通过“自定义URI方案”将
code
返回给应用程序,因此该
code
可能会被恶意软件拦截(因为它注册的程序与合法应用程序相同)。 之后,恶意软件将
code
更改为
access_token
并获得对用户数据的访问权限。
如何保护自己? 在某些情况下,可以使用进程间通信机制;我们将在下面讨论它们。 在一般情况下,您需要应用一种称为
代码交换证明密钥的方案。 其实质反映在下图中。
图片取自https://tools.ietf.org/html/rfc7636#section-1.1在来自客户端的请求中,有几个附加参数:
code_verifier
,
code_challenge
(在
t(code_verifier)
)和
code_challenge_method
(在
t_m
图上)。
Code_verifier
是
长度至少为256位的随机数
, 只能使用一次 。 也就是说,对于
每个 code
请求
code
客户端必须生成一个新的
code_verifier
。
Code_challenge_method
是转换函数的名称,通常是SHA-256。
Code_challenge
是一个
code_verifier
,已对其
code_challenge_method
转换并将其编码在Safe Base64 URL中。
在请求
code
时,必须基于对
code_verifier
的拦截(例如,来自设备的系统日志),将
code_challenge
转换为
code_verifier
以防止受到攻击矢量的
code_verifier
。
如果用户的设备
不支持 SHA-256,则
允许降级,直到缺少code_verifier转换为止 。 在所有其他情况下,您必须使用SHA-256。
该方案的工作原理如下:
- 客户端生成一个
code_verifier
并记住它。 - 客户端选择
code_challenge_method
并从code_verifier
获得code_verifier
。 - [步骤A]客户请求
code
,其中code_challenge
和code_challenge_method
添加到请求中。 - [步骤B]提供程序会记住服务器上的
code_challenge
和code_challenge_method
,并将code
返回code
客户端。 - [步骤C]客户端请求
access_token
,并将access_token
添加到code_verifier
。 - 提供程序从传入的
code_verifier
接收到code_challenge
,然后对照他记得的code_challenge
进行检查。 - [步骤D]如果值匹配,则提供者
access_token
客户端发出access_token
。
让我们
code_challenge
为什么
code_challenge
允许
code_challenge
保护自己免受代码拦截攻击。 为此,我们将经历获取
access_token
的阶段。
- 首先,合法的应用程序请求
code
( code_challenge
和code_challenge_method
与请求一起发送 )。 - 恶意软件拦截
code
(但不会拦截code_challenge
,因为响应中没有code_challenge
)。 - 恶意软件请求
access_token
(具有有效code
,但没有有效code_verifier
)。 - 服务器注意到
code_challenge
不匹配,并引发错误。
请注意,攻击者无法猜测
code_verifier
(随机256位!)或在日志中的某个位置找到它(
code_verifier
传输一次)。
如果将所有内容简化为一个短语,则
code_challenge
允许服务提供商回答以下问题:“
access_token
由请求
code
的同一客户端应用程序请求,还是由另一个请求
code
客户端应用程序请求?”
OAuth 2.0 CSRF
在移动设备上,OAuth 2.0通常用作身份验证机制。 我们记得,通过OAuth 2.0进行的身份验证与授权不同,因为OAuth 2.0漏洞会影响服务客户端(而不是服务提供商)一侧的用户数据。 因此,对OAuth 2.0的CSRF攻击使您可以窃取他人的帐户。
考虑使用出租车客户端应用程序和provider.com提供程序示例的针对OAuth 2.0的CSRF攻击。 首先,攻击者在其设备上登录Attacker@provider.com并接收出租车
code
。 之后,攻击者中断OAuth 2.0进程并生成一个链接:
com.taxi.app://oauth?
code=b57b236c9bcd2a61fcd627b69ae2d7a6eb5bc13f2dc25311348ee08df43bc0c4
然后,攻击者以出租车管理部门的信件或短信为幌子,向受害者发送链接。 受害人
access_token
链接,在她的手机上打开出租车应用程序,该应用程序接收
access_token
,结果受害人最终进入了
攻击者的出租车帐户。 受害人不知道渔获,使用此帐户:旅行,输入数据等。
现在,攻击者可以随时登录受害者的出租车帐户,因为他已绑定到
attacker@provider.com
。 CSRF的登录攻击允许窃取帐户。
CSRF攻击通常使用CSRF令牌(也称为
state
)进行保护,OAuth 2.0也不例外。 如何使用CSRF令牌:
- 客户端应用程序生成CSRF令牌并将其存储在用户的移动设备上。
- 客户端应用程序在请求
code
包含CSRF令牌。 - 服务器在响应中返回相同的CSRF令牌以及代码。
- 客户端应用程序比较传入和存储的CSRF令牌。 如果值匹配,则过程继续。
CSRF令牌要求:
随机数至少为256位,可从良好的伪随机序列源获得。
简而言之,CSRF令牌允许客户端应用程序回答以下问题:“我是在开始获取
access_token
还是有人在试图欺骗我?”
假冒合法客户的恶意软件
某些恶意软件可以模仿合法应用程序,并代表它们提出同意屏幕(同意屏幕是用户看到的屏幕:“我同意授予...的访问权限”)。 不专心的用户可以单击“允许”,结果,恶意软件可以访问用户数据。
Android和iOS提供了用于相互验证应用程序的机制。 提供者应用程序可以验证客户端应用程序的合法性,反之亦然。
不幸的是,如果OAuth 2.0机制通过浏览器使用流,那么您将无法防御这种攻击。
其他攻击
我们研究了移动OAuth 2.0特有的攻击。 但是,请不要忘记对常规OAuth 2.0的攻击:
redirect_uri
欺骗,不安全连接上的流量拦截等。 您可以
在此处阅读有关它们的更多信息。
怎么办
我们了解了OAuth 2.0协议的工作原理,并弄清了该协议在移动设备上的实现中存在哪些漏洞。 现在,让我们从各个方面来构建一个安全的移动OAuth 2.0方案。
OAuth 2.0的好坏
让我们从如何正确显示同意屏幕开始。 在移动设备上,有两种方法可以从本机应用程序打开网页(本机应用程序示例:Mail.Ru Mail,VK,Facebook)。

第一种方法称为“浏览器自定义选项卡”(在左图中)。
注意 :Android上的“浏览器自定义”选项卡在iOS上称为Chrome“自定义”选项卡。 实际上,这是一个正常的浏览器标签,直接在应用程序中显示,即 应用程序之间没有视觉切换。
第二种方法称为“提升WebView”(如右图所示),相对于移动OAuth 2.0,我认为这很糟糕。
WebView是用于本机应用程序的独立浏览器。
“独立
浏览器 ”是指WebView不允许从Safari和Chrome浏览器访问cookie,存储,缓存,历史记录和其他数据。 反之亦然:Safari和Chrome无法访问WebView数据。
“
本机应用程序的浏览器 ”是指引发WebView的本机应用程序具有对Cookie,存储,缓存,历史记录和其他WebView数据的
完全访问权限。
现在想象:用户按下“使用...登录”按钮,恶意应用程序的WebView向服务提供商询问其用户名和密码。
在所有方面立即失败:
- 用户从应用程序中服务提供商的帐户输入用户名和密码,可以轻松窃取此数据。
- OAuth 2.0最初是为了避免输入服务提供商的用户名和密码而开发的。
- 用户习惯于在任何地方输入登录名和密码, 网络钓鱼的可能性增加。
鉴于所有论点都反对WebView,结论本身表明:提高“浏览器自定义选项卡以显示同意”屏幕。
如果有任何人赞成使用WebView而不是“浏览器自定义选项卡”,请在评论中写出来,我将不胜感激。
安全移动OAuth 2.0方案
我们将使用授权代码授予方案,因为它允许我们添加
code_challenge
并保护
code_challenge
免受代码拦截攻击。
图片取自https://tools.ietf.org/html/rfc8252#section-4.1代码请求(步骤1-2)将如下所示:
https://o2.mail.ru/code?
redirect_uri=com.mail.cloud.app%3A%2F%2Foauth&
anti_csrf=927489cb2fcdb32e302713f6a720397868b71dd2128c734181983f367d622c24& code_challenge=ZjYxNzQ4ZjI4YjdkNWRmZjg4MWQ1N2FkZjQzNGVkODE1YTRhNjViNjJjMGY5MGJjNzdiOGEzMDU2ZjE3NGFiYw%3D%3D&
code_challenge_method=S256&
scope=email%2Cid&
response_type=code&
client_id=984a644ec3b56d32b0404777e1eb73390c
在步骤3中,浏览器收到重定向的响应:
com.mail.cloud.app://outh?
code=b57b236c9bcd2a61fcd627b69ae2d7a6eb5bc13f2dc25311348ee08df43bc0c4&
anti_csrf=927489cb2fcdb32e302713f6a720397868b71dd2128c734181983f367d622c24
在步骤4中,浏览器打开“自定义URI方案”,并将
code
和CSRF令牌传递给客户端应用程序。
要求
access_token
(步骤5):
https://o2.mail.ru/token?
code_verifier=e61748f28b7d5daf881d571df434ed815a4a65b62c0f90bc77b8a3056f174abc&
code=b57b236c9bcd2a61fcd627b69ae2d7a6eb5bc13f2dc25311348ee08df43bc0c4&
client_id=984a644ec3b56d32b0404777e1eb73390c
最后一步返回一个带有
access_token
的响应。
通常,上述方案是安全的,但是在某些特殊情况下,可以使OAuth 2.0变得更简单,更安全。
Android IPC
Android具有在进程之间进行双向数据交换的机制:IPC(进程间通信)。 与自定义URI方案相比,首选IPC的原因有两个:
- 打开IPC通道的应用程序可以通过其证书来验证打开的应用程序的真实性。 反之亦然:打开的应用程序可以验证打开它的应用程序的真实性。
- 通过IPC通道发送请求,发送方可以通过同一通道接收响应。 连同相互验证(项目1)一起,这意味着没有第三方进程可以拦截
access_token
。

因此,我们可以使用
隐式授予,并大大简化移动OAuth 2.0方案。 没有
code_challenge
和CSRF令牌。 此外,我们将能够保护自己免受模仿合法客户端的恶意软件的侵害,从而窃取用户帐户。
客户SDK
除了实施上述的安全移动OAuth 2.0方案外,提供商还应为其客户开发一个SDK。 这将有助于在客户端实施OAuth 2.0,同时减少错误和漏洞的数量。
得出结论
对于OAuth 2.0提供程序,我编写了“安全移动OAuth 2.0检查表”:
- 坚实的基础至关重要。 在移动OAuth 2.0的情况下,基础是我们选择实现的方案或协议。 实施自己的OAuth 2.0架构时,很容易出错。 其他人已经填补了障碍并做出了结论,从错误中吸取教训并立即进行安全实施并没有错。 通常,最安全的移动OAuth 2.0方案是“怎么做?”部分中的方案。
Access_token
和其他敏感数据存储在iOS下-钥匙串中,Android下-内部存储中。 这些存储库是专门为此类目的而设计的。 如有必要,您可以在Android中使用Content Provider,但必须对其进行安全配置。Code
应该是一次性的,寿命很短。- 为了防止代码被拦截,请使用
code_challenge
。 - 为了防止登录时遭受CSRF攻击,请使用CSRF令牌。
- 不要将WebView用于同意屏幕,请使用“浏览器自定义选项卡”。
- 如果
Client_secret
没有存储在后端,则它Client_secret
无用的。 不要将其提供给公共客户。 - 在所有地方都使用HTTPS,禁止降级为HTTP。
- 遵循标准中的加密建议(密码选择,令牌长度等)。 您可以复制数据并找出这样做的原因,但不能进行加密 。
- 在客户端应用程序中,验证您为OAuth 2.0打开的用户,然后在提供商应用程序中,检查谁为OAuth 2.0打开的用户。
- 请注意通常的OAuth 2.0漏洞 。 移动OAuth 2.0扩展并补充了常规OAuth 2.0,因此没有人取消过
redirect_uri
检查以获取完全匹配以及常规OAuth 2.0的其他建议。 - 确保向客户提供SDK。 客户端中的代码错误和漏洞更少,并且对他来说,实现您的OAuth 2.0更容易。
读什么
- [RFC]适用于本机应用程序的OAuth 2.0 https://tools.ietf.org/html/rfc8252
- 适用于移动和桌面应用程序的Google OAuth 2.0 https://developers.google.com/identity/protocols/OAuth2InstalledApp
- [RFC] OAuth公共客户端用于代码交换的证明密钥https://tools.ietf.org/html/rfc7636
- OAuth 2.0竞赛条件https://hackerone.com/reports/55140
- [RFC] OAuth 2.0威胁模型和安全注意事项https://tools.ietf.org/html/rfc6819
- 对常规OAuth 2.0的攻击https://sakurity.com/oauth
- [RFC] OAuth 2.0动态客户端注册协议https://tools.ietf.org/html/rfc7591
致谢
感谢所有帮助撰写本文的人员,特别是谢尔盖·贝洛夫(Sergey Belov),安德烈·苏敏(Andrey Sumin),安德烈·拉宾兹(Andrey Labunts)(
@isciurus )和Daria Yakovleva。