OAuth 2.0 常见漏洞与攻击技术深度解析

4 阅读15分钟

常见 OAuth 漏洞

2025年1月30日 - 作者:Jose Catalan, Szymon Drosdzol

OAuth2 的流行使其成为攻击者的主要目标。虽然它简化了用户登录,但其复杂性可能导致配置错误,从而产生安全漏洞。一些更复杂的漏洞不断重现,因为该协议的内部工作原理并不总是被充分理解。为了改变这一点,我们决定编写一份关于针对 OAuth 实现的已知攻击的综合指南。此外,我们还创建了一份全面的清单。这对于测试人员和开发人员快速评估其实现是否安全应该很有用。

立即下载 OAuth 安全速查表!

OAuth 简介

OAuth 术语

OAuth 是一个复杂的协议,涉及许多参与者和活动部件。在深入了解其内部工作原理之前,让我们先回顾一下它的术语:

  • 资源所有者 (Resource Owner):可以授予对受保护资源访问权限的实体。通常,这是最终用户。
  • 客户端 (Client):代表资源所有者请求访问受保护资源的应用程序。
  • 资源服务器 (Resource Server):托管受保护资源的服务器。这是您想要访问的 API。
  • 授权服务器 (Authorization Server):对资源所有者进行身份验证并在获得适当授权后颁发访问令牌的服务器。例如,Auth0。
  • 用户代理 (User Agent):资源所有者用来与客户端交互的代理(例如,浏览器或本地应用程序)。

参考

OAuth 常见流程

针对 OAuth 的攻击依赖于挑战授权流程所基于的各种假设。因此,理解这些流程对于有效地攻击和防御 OAuth 实现至关重要。以下是最流行流程的高级描述。

隐式流程 (Implicit Flow)

隐式流程最初是为无法安全存储客户端凭证的本地应用或单页应用设计的。然而,现在不鼓励使用它,并且它未被包含在 OAuth 2.1 规范中。尽管如此,在 Open ID Connect (OIDC) 中,它仍然是检索 id_token 的一种可行的身份验证解决方案。 在此流程中,用户代理被重定向到授权服务器。在执行身份验证和同意后,授权服务器直接返回访问令牌,使其可供资源所有者访问。这种方法将访问令牌暴露给用户代理,而用户代理可能通过 XSS 或有缺陷的 redirect_uri 验证等漏洞受到威胁。如果 response_mode 未设置为 form_post,隐式流程会将访问令牌作为 URL 的一部分进行传输。

参考

授权码流程 (Authorization Code Flow)

授权码流程是 Web 应用程序中使用最广泛的 OAuth 流程之一。与直接向授权服务器请求访问令牌的隐式流程不同,授权码流程引入了一个中间步骤。在此过程中,用户代理首先检索授权码,然后应用程序将其与客户端凭证一起交换为访问令牌。这个额外的步骤确保只有客户端应用程序可以访问访问令牌,从而防止用户代理看到它。 此流程仅适用于机密应用程序,例如常规 Web 应用程序,因为代码交换请求中包含了应用程序客户端凭证,并且必须由客户端应用程序安全地存储。

参考

带 PKCE 的授权码流程 (Authorization Code Flow with PKCE)

OAuth 2.0 提供了授权码流程的一个版本,它使用了用于代码交换的证明密钥 (PKCE)。此 OAuth 流程最初是为无法存储客户端密钥的应用程序(例如本地应用或单页应用)设计的,但它已成为 OAuth 2.1 规范中的主要建议。 在默认的授权码流程中添加了两个新参数:一个随机生成的值,称为 code_verifier,以及它的转换版本 code_challenge

  1. 首先,客户端创建并记录一个秘密的 code_verifier,并派生出一个转换版本 t(code_verifier),称为 code_challenge,它与转换方法 t_m 一起在授权请求中发送。
  2. 然后,客户端在访问令牌请求中连同 code_verifier 秘密一起发送授权码。
  3. 最后,授权服务器转换 code_verifier 并将其与 t(code_verifier) 进行比较。

可用的转换方法 (t_m) 如下:

  • plain: code_challenge = code_verifier
  • S256: code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

请注意,使用带有自定义 redirect_uri 方案(如 example.app://)的默认授权码流程可能允许恶意应用程序与合法的 OAuth 2.0 应用程序一起注册为该自定义方案的处理程序。如果发生这种情况,恶意应用程序可以拦截授权码并将其交换为访问令牌。有关更多详细信息,请参阅 OAuth 重定向方案劫持。 使用 PKCE,拦截授权响应不会允许上述攻击场景,因为攻击者只能访问 authorization_code,但无法获得访问令牌请求所需的 code_verifier 值。

参考

客户端凭证流程 (Client Credentials Flow)

客户端凭证流程是为机器对机器 (M2M) 应用程序设计的,例如守护程序或后端服务。当客户端同时也是资源所有者时,这非常有用,从而无需用户代理身份验证。此流程允许客户端通过提供客户端凭证直接检索访问令牌。

参考

设备授权流程 (Device Authorization Flow)

设备授权流程专为联网设备设计,这些设备要么缺乏用于基于用户代理的授权的浏览器,要么在授权流程期间输入受限而无法进行基于文本的身份验证。 此流程允许智能电视、媒体主机、数码相框或打印机等设备上的 OAuth 客户端使用单独设备上的用户代理来获得用户授权以访问受保护资源。 在此流程中,客户端应用程序首先从授权服务器检索用户代码和验证 URL。然后,它指示用户代理使用提供的用户代码和验证 URL 通过不同的设备进行身份验证和同意。

参考

资源所有者密码凭证流程 (Resource Owner Password Credentials Flow)

此流程要求资源所有者完全信任客户端拥有其授权服务器的凭证。它专为无法使用基于重定向的流程的用例而设计,尽管它已在最近的 OAuth 2.1 RFC 规范中被移除,不推荐使用。 该流程不是将资源所有者重定向到授权服务器,而是将用户凭证发送到客户端应用程序,然后客户端应用程序将其转发给授权服务器。

参考

攻击

在本节中,我们将介绍针对 OAuth 的常见攻击及其基本修复策略。

CSRF

OAuth CSRF 是一种针对 OAuth 流程的攻击,其中消耗授权码的浏览器与发起流程的浏览器不同。攻击者可以利用此攻击迫使受害者消耗其授权码,导致受害者使用攻击者的授权上下文进行连接。 根据应用程序的上下文,影响可能从低到高不等。在任何情况下,确保用户控制其操作的授权上下文并且不能被强制进入另一个上下文至关重要。

缓解措施 OAuth 规范建议使用 state 参数来防止 CSRF 攻击。 “[state 是] 客户端用来维护请求和回调之间状态的不透明值。授权服务器在将用户代理重定向回客户端时包含此值。该参数应用于防止跨站点请求伪造 (CSRF)。”

参考

  • Justin Richer, Antonio Sanso, (2017), OAuth 2 In Action

重定向攻击

实现良好的授权服务器会在将用户代理重定向回客户端之前验证 redirect_uri 参数。redirect_uri 值的白名单应为每个客户端单独配置。这种设计确保用户代理只能被重定向到客户端,并且授权码将仅披露给该客户端。相反,如果授权服务器忽略或错误实现了此验证,恶意行为者可以操纵受害者完成一个流程,该流程会将其授权码披露给不受信任的方。 在最简单的形式中,当 redirect_uri 验证完全缺失时,可以利用以下流程进行利用:

当验证实现不当时,也可能出现此漏洞。唯一正确的方法是通过比较精确的 redirect_uri(包括源(方案、主机名、端口)和路径)进行验证。 常见错误包括:

  • 仅验证源/域
  • 允许子域
  • 允许子路径
  • 允许通配符

如果给定的源包含一个具有开放重定向漏洞或用户控制内容的页面的 URL,则它们可能被滥用以通过 Referer 标头或开放重定向窃取代码。 另一方面,以下疏忽:

  • 部分路径匹配
  • 误用正则表达式匹配 URI

可能导致通过制作恶意 URL 产生各种绕过,从而导向不受信任的源。

参考

  • Justin Richer, Antonio Sanso, (2017), OAuth 2 In Action

可变声明攻击 (Mutable Claims Attack)

根据 OAuth 规范,用户由 sub 字段唯一标识。但是该字段没有标准格式。因此,根据授权服务器的不同,使用了许多不同的格式。一些客户端应用程序为了跨多个授权服务器制定统一的用户识别方式,会回退到用户句柄或电子邮件。然而,这种方法可能很危险,具体取决于所使用的授权服务器。一些授权服务器不保证此类用户属性的不可变性。更糟糕的是,在某些情况下,用户自己可以随意更改这些属性。在这种情况下,帐户接管可能发生。 其中一个案例是,当实现“使用 Microsoft 登录”功能以使用电子邮件字段来识别用户时。在这种情况下,攻击者可以在 Azure 上创建自己的 AD 组织(此处为 doyensectestorg),然后可用于执行“使用 Microsoft 登录”。虽然放置在 sub 中的对象 ID 字段对于给定用户是不可变的且无法伪造,但电子邮件字段完全由用户控制,不需要任何验证。

在上面的截图中,创建了一个示例用户,可用于在客户端接管 victim@gmail.com 的帐户,该客户端使用电子邮件字段进行用户识别。

参考

客户端混淆攻击 (Client Confusion Attack)

当应用程序实现用于身份验证的 OAuth 隐式流程时,他们应验证最终提供的令牌是为该特定客户端 ID 生成的。如果未执行此检查,攻击者就有可能使用为不同客户端 ID 生成的访问令牌。 想象一下,攻击者创建了一个公共网站,允许用户使用 Google 的 OAuth 隐式流程登录。假设有成千上万的人连接到该托管网站,攻击者就可以访问他们的、为攻击者网站生成的 Google OAuth 访问令牌。 如果这些用户中的任何一个在易受攻击的网站上已经拥有一个不验证访问令牌的帐户,攻击者就能够提供为不同客户端 ID 生成的受害者访问令牌,并能够接管受害者的帐户。 一个为身份验证而实现的安全 OAuth 隐式流程如下所示:

如果未执行第 8 到第 10 步,并且未验证令牌的客户端 ID,则可能执行以下攻击:

补救措施 值得注意的是,即使客户端使用了更安全的流程(例如显式流程),它也可能接受访问令牌——这有效地允许降级到隐式流程。此外,如果应用程序使用访问令牌作为会话 cookie 或授权标头,则可能存在漏洞。在实践中,确保从不接受来自用户控制参数的访问令牌可以尽早打破利用链。除此之外,我们建议按照上述第 8 到第 10 步进行令牌验证。

参考

权限升级攻击 (Scope Upgrade Attack)

使用授权码授予类型时,用户数据通过安全的服务器到服务器通信进行请求和发送。 如果授权服务器接受并隐式信任在访问令牌请求中发送的 scope 参数(请注意,在授权码流程的访问令牌请求的 RFC 中未指定此参数),恶意应用程序可能会尝试通过在访问令牌请求中发送权限更高的范围来升级从用户回调中检索到的授权码的范围。 生成访问令牌后,资源服务器必须验证每个请求的访问令牌。此验证取决于访问令牌格式,常用的格式如下:

  • JWT 访问令牌:使用这种访问令牌,资源服务器只需要检查 JWT 签名,然后检索 JWT 中包含的数据(client_idscope 等)。
  • 随机字符串访问令牌:由于这种令牌不包含任何附加信息,资源服务器需要从授权服务器检索令牌信息。

缓解措施 遵循 RFC 指南,scope 参数不应在授权码流程的访问令牌请求中发送,尽管它可以在其他流程(如资源所有者密码凭证授予)中指定。 授权服务器应忽略 scope 参数或验证它与之前在授权请求中提供的范围匹配。

参考

重定向方案劫持 (Redirect Scheme Hijacking)

当需要在移动设备上使用 OAuth 时,移动应用程序扮演 OAuth 用户代理的角色。为了让它们能够接收带有授权码的重定向,开发人员通常依赖于自定义方案的机制。但是,多个应用程序可以在给定设备上注册给定的方案。这打破了 OAuth 关于客户端是唯一控制已配置 redirect_uri 的假设,并且如果在受害者的设备上安装了恶意应用程序,可能导致授权码被接管。 Android Intent URI 具有以下结构: <scheme>://<host>:<port>[<path>|<pathPrefix>|<pathPattern>|<pathAdvancedPattern>|<pathSuffix>] 因此,例如,URI com.example.app://oauth 描述了一个具有 scheme=com.example.apphost=oauth 的 Intent。为了接收这些 Intent,Android 应用程序需要导出一个类似于以下的 Activity:

    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:host="oauth" android:scheme="=com.example.app"/>
    </intent-filter>

Android 系统在定义 Intent 过滤器时相当宽松。过滤器细节越少,捕获的网络越广,潜在的 URI 越多。例如,如果只提供了 scheme,则无论其主机、路径等如何,都将捕获该 scheme 的所有 Intent。 如果有多个应用程序可以捕获给定的 Intent,系统将让用户决定使用哪个,这意味着重定向接管需要用户交互。然而,根据上述知识,可以尝试创建绕过方法,具体取决于合法应用程序的过滤器是如何创建的。矛盾的是,原始开发人员越具体,就越容易制作绕过方法并在没有用户交互的情况下接管重定向。详细地说,Ostorlab 创建了以下流程图来快速评估是否可能:

建议 对于显式授权码流程不可行的情况,因为客户端不能被信任安全地存储客户端密钥,我们推荐使用带 PKCE 的授权码流程。此外,为了恢复授权服务器和 redirect_uri 目标之间的信任关系,建议使用 Android 的可验证链接和 iOS 的关联域机制。 简而言之,Android 为 Intent 过滤器引入了 autoVerify 属性。具体来说,开发人员可以创建类似于以下的 Intent 过滤器:

<intent-filter android:autoVerify="true">
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="http" />
  <data android:scheme="https" />
  <data android:host="www.example.com" />
</intent-filter>

当以这种方式定义 Intent 过滤器时,Android 系统会验证定义的主机是否确实属于应用程序的创建者。具体来说,主机需要向关联域发布一个 /.well-known/assetlinks.json 文件,列出给定的 APK,以便被允许处理给定的链接:

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example",
    "sha256_cert_fingerprints":
    ["14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"]
  }
}]

得益于这种设计,恶意应用程序无法为已经声明的主机注册自己的 Intent 过滤器,尽管这仅在处理的方案不是自定义的情况下有效。例如,如果应用程序处理 com.example.app:// 方案,则无法赋予额外优先级,用户将不得不在为该特定方案实现处理程序的应用程序之间进行选择。

参考

总结

本文提供了针对 OAuth 协议的攻击和防御的综合列表。在发布本文的同时,我们还为开发人员和测试人员发布了一份全面的速查表。 下载 OAuth 安全速查表:

由于该领域经常出现新的研究和发展,我们并不声称完全了解所有细微之处。如果您对如何改进此摘要有任何建议,请随时联系作者。我们将很高兴更新这篇博文,以便它可以被视为任何对该主题感兴趣的人的全面资源。 fKDlTFcPcc1J0xpM4hJULkpPJ/hcSH9zxhgau8h8gqVkSGnWn6Jx3PsjncieZ2N2sumMSTAgWX07fMkZx57FS7RBPGE60B4sF/cY5ALH59M=