什么是 OAuth?
OAuth 是一种常用的授权框架,它允许网站和 Web 应用程序请求对其他应用程序中用户账户的有限访问。关键的是,OAuth 允许用户在不向请求应用程序暴露其登录凭证的情况下授予这种访问权限。这意味着用户可以微调他们想要共享的数据,而不是将账户的全部控制权交给第三方。
例如,一个应用程序可能使用 OAuth 请求访问您的电子邮件联系人列表,以便它可以建议您与之连接的人。然而,同样的机制也被用于提供第三方身份验证服务,允许用户使用他们在不同网站上拥有的账户登录。
OAuth 2.0 是如何工作的
OAuth 2.0 通过定义三个不同当事人之间的一系列交互来实现,即客户端应用程序、资源所有者和 OAuth 服务提供商。
客户端应用程序 - 想要访问用户数据的网站或 Web 应用程序
资源所有者 - 想要访问其数据的客户端应用程序的用户
OAuth 服务提供商 - 控制用户数据并允许访问的网站或应用程序。他们通过提供 API 来支持 OAuth,以与授权服务器和资源服务器交互
重点关注“授权码”和“隐式”授权类型,因为这两种类型是最常见的。从广义上讲,这两种授权类型都涉及以下阶段
1:客户端应用程序请求访问用户数据的一部分,指定他们想要使用的授权类型以及他们想要什么样的访问权限。
2:用户被提示登录 OAuth 服务并明确同意请求的访问权限。
3:客户端应用程序接收一个唯一的访问令牌,证明他们已从用户那里获得访问请求数据的权限。具体如何实现这取决于授权类型。
4:客户端应用程序使用此访问令牌来调用 API,从资源服务器获取相关数据。
对于 OAuth 认证机制,基本的 OAuth 流程保持大致相同;主要区别在于客户端应用程序如何使用它接收到的数据。从最终用户的角度来看,OAuth 认证的结果与基于 SAML 的单点登录(SSO)大致相似。在这些材料中,我们将专注于这种类似 SSO 用例中的漏洞
OAuth 认证通常如下实现:
1:用户选择使用社交媒体账户登录。客户端应用程序随后使用社交媒体网站的 OAuth 服务请求访问一些可以用来识别用户的数据。例如,这可能是与账户注册的电子邮件地址。
2:接收到访问令牌后,客户端应用程序从资源服务器请求这些数据,通常是从一个专门的 /userinfo
端点。
3:一旦收到数据,客户端应用程序就会用它来代替用户名进行用户登录。它从授权服务器收到的访问令牌通常被用来代替传统的密码。
OAuth 授权类型
对于基本的 OAuth,客户端应用程序可以请求访问的权限范围是每个 OAuth 服务的独特值。由于权限范围的名称只是一个任意的文本字符串,因此格式在各个提供者之间可能会有很大的差异。有些甚至使用完整的 URI 作为权限范围的名称,类似于 REST API 端点。例如,当请求读取用户联系列表的访问权限时,权限范围的名称可能根据所使用的 OAuth 服务而采取以下任何一种形式:
scope=contacts scope=contacts.read scope=contact-list-r scope=https://oauth-authorization-server.com/auth/scopes/user/contacts.readonly
当 OAuth 用于身份验证时,通常会使用标准化的 OpenID Connect 权限范围。例如,权限范围 openid profile
将授予客户端应用程序读取用户预定义的基本信息的访问权限,例如他们的电子邮件地址、用户名等
OAuth 2.0 认证漏洞是如何产生的?
OAuth 认证漏洞部分原因是 OAuth 规范本身相对模糊且灵活。尽管每个授权类型的基本功能都需要一些强制性组件,但绝大多数实现都是可选的。这包括许多必要的配置设置,用于保护用户数据安全。简而言之,存在很多不良实践的机会。
识别 OAuth 身份验证
识别应用程序是否使用 OAuth 认证相对简单。如果您看到有使用您账户从不同网站登录的选项,这通常意味着 OAuth 正在被使用。
识别 OAuth 认证最可靠的方法是通过 代理您的流量,并在使用此登录选项时检查相应的 HTTP 消息。无论使用哪种 OAuth 授权类型,流程的第一个请求都将始终是对包含特定于 OAuth 的多个查询参数的 /authorization
端点的请求。特别是,请注意 client_id
、 redirect_uri
和 response_type
参数。例如,授权请求通常看起来像这样:
GET /authorization?client_id=12345&redirect_uri=https://client-app.com/callback&response_type=token&scope=openid%20profile&state=ae13d489bd00e3c24 HTTP/1.1 Host: oauth-authorization-server.com
Recon 重置
不言而喻,你应该研究构成 OAuth 流程的各种 HTTP 交互 - 我们稍后会讨论一些需要注意的具体事项。如果使用外部 OAuth 服务,你应该能够从发送授权请求的主机名中识别出特定的提供者。由于这些服务提供公共 API,通常会有详细的文档可供参考,这些文档应该会告诉你各种有用的信息,例如端点的确切名称以及正在使用的配置选项
一旦你知道授权服务器的主机名,你应该始终尝试向以下标准端点发送 GET
请求:
/.well-known/oauth-authorization-server
/.well-known/openid-configuration
通常会返回一个包含关键信息的 JSON 配置文件,例如可能支持的其他功能的详细信息。这有时会提示您关于更广泛的攻击面和可能未在文档中提及的支持功能
利用 OAuth 身份验证漏洞
OAuth 客户端应用程序中的漏洞
不恰当的隐式授权类型实现
由于通过浏览器发送访问令牌引入的危险,隐式授权类型主要推荐用于单页应用程序。然而,由于其相对简单,它也常用于经典客户端-服务器 Web 应用程序。
在这个流程中,访问令牌通过用户的浏览器作为 URL 片段从 OAuth 服务发送到客户端应用程序。然后客户端应用程序使用 JavaScript 访问令牌。问题是,如果应用程序想在用户关闭页面后保持会话,它需要将当前用户数据(通常是用户 ID 和访问令牌)存储在某个地方。
为了解决这个问题,客户端应用程序通常会以 POST
请求的形式将此数据提交给服务器,然后为用户分配一个会话 cookie,从而有效地登录。此请求大致等同于可能作为经典基于密码登录的一部分发送的表单提交请求。然而,在这种情况下,服务器没有任何秘密或密码可以与提交的数据进行比较,这意味着它是隐含信任的。
在隐式流中,此 POST
请求通过其浏览器暴露给攻击者。因此,如果客户端应用程序没有正确检查访问令牌是否与请求中的其他数据匹配,这种行为可能导致严重漏洞。在这种情况下,攻击者可以简单地更改发送给服务器的参数来冒充任何用户。
CSRF 保护存在缺陷
state
参数理想情况下应包含一个不可猜测的值,例如与用户会话相关联的某物的哈希值,当 OAuth 流程首次启动时。然后,这个值作为客户端应用的 CSRF 令牌,在客户端应用和 OAuth 服务之间来回传递。因此,如果您注意到授权请求没有发送 state
参数,这对攻击者来说非常有趣。这可能意味着他们可以在欺骗用户浏览器完成之前自行启动 OAuth 流程,类似于传统的 CSRF 攻击。这取决于客户端应用如何使用 OAuth,可能会产生严重后果。
考虑一个允许用户使用经典密码机制或通过 OAuth 将账户链接到社交媒体配置文件进行登录的网站。在这种情况下,如果应用程序未能使用 state
参数,攻击者可能会通过将其绑定到自己的社交媒体账户来篡夺受害者用户在客户端应用程序上的账户。
请注意,如果网站仅允许用户通过 OAuth 登录,则 state
参数可能不那么关键。然而,不使用 state
参数仍然可能允许攻击者构建登录 CSRF 攻击,其中用户被诱骗登录到攻击者的账户
泄露授权码和访问令牌
当 OAuth 服务的配置本身允许攻击者窃取与其他用户账户关联的授权代码或访问令牌时。通过窃取有效的代码或令牌,攻击者可能能够访问受害者的数据。最终,这可能导致他们的账户完全被破坏——攻击者可能能够在注册了此 OAuth 服务的任何客户端应用程序上以受害者用户身份登录。
根据授权请求中指定的 redirect_uri
参数,根据授权类型,通过受害者的浏览器将代码或令牌发送到 /callback
端点。如果 OAuth 服务无法正确验证此 URI,攻击者可能能够构建类似 CSRF 的攻击,诱使受害者的浏览器启动 OAuth 流程,将代码或令牌发送到攻击者控制的 redirect_uri
在授权码流的情况下,攻击者可以在代码被使用之前窃取受害者的代码。然后,他们可以将此代码发送到客户端应用的合法 /callback
端点(原始 redirect_uri
)以获取对用户账户的访问权限。在这种情况下,攻击者甚至不需要知道客户端密钥或生成的访问令牌。只要受害者与 OAuth 服务有有效的会话,客户端应用就会代表攻击者完成代码/令牌交换,然后在将他们登录到受害者的账户之前。
请注意,使用 state
或 nonce
保护并不一定能防止这些攻击,因为攻击者可以从自己的浏览器生成新的值
更安全的授权服务器在交换代码时将需要发送一个 redirect_uri
参数。服务器可以检查这个参数是否与它在初始授权请求中收到的匹配,如果不匹配则拒绝交换。由于这种情况发生在服务器之间的安全回路上,攻击者无法控制这个第二个 redirect_uri
参数
错误的 redirect_uri 验证
客户端应用程序在注册 OAuth 服务时提供其真实回调 URI 的白名单是一种最佳实践。这样,当 OAuth 服务接收到新的请求时,它可以验证 redirect_uri
参数是否与该白名单匹配。在这种情况下,提供外部 URI 可能会引发错误。然而,可能仍然存在绕过此验证的方法
一些实现允许通过仅检查字符串是否以正确的字符序列开头来检查一系列子目录,即批准的域名。你应该尝试删除或添加任意路径、查询参数和片段,看看你能否在不触发错误的情况下更改它们。
某些服务器也特别处理 localhost
URI,因为它们在开发过程中经常被使用。在某些情况下,任何以 localhost
开头的重定向 URI 可能会在生产环境中意外允许。这可能会让你通过注册如 localhost.evil-user.net
这样的域名来绕过验证。
需要注意的是,您不应仅限于单独探测 redirect_uri
参数。在野外,您通常需要尝试不同参数组合的变化。有时更改一个参数可能会影响其他参数的验证。例如,将 response_mode
从 query
更改为 fragment
有时可以完全改变 redirect_uri
的解析,使您能够提交否则会被阻止的 URI。同样,如果您注意到支持 web_message
响应模式,这通常允许在 redirect_uri
中使用更广泛的子域。
通过代理页面窃取代码和访问令牌
到这一阶段,你应该对 URI 中可以篡改的部分有相对较好的理解。现在的关键是利用这些知识尝试访问客户端应用程序本身的更广泛的攻击面。换句话说,尝试确定是否可以更改 redirect_uri
参数以指向白名单域上的任何其他页面。
尝试找到可以成功访问不同子域名或路径的方法。例如,默认 URI 通常位于 OAuth 特定的路径上,如 /oauth/callback
,这不太可能有任何有趣的子目录。然而,你可能能够使用目录遍历技巧来提供域上的任意路径。例如:
https://client-app.com/oauth/callback/../../example/path
一旦确定可以设置为重定向 URI 的其他页面,您应该对这些页面进行审计,以发现可能用于泄露代码或令牌的额外漏洞。对于授权码流程,您需要找到一个可以访问查询参数的漏洞,而对于隐式授权类型,则需要提取 URL 片段
注意,对于隐式授权类型,窃取访问令牌不仅使您能够登录到受害者的客户端应用程序账户,由于整个隐式流程都通过浏览器进行,您还可以使用该令牌向 OAuth 服务的资源服务器发起自己的 API 调用。这可能会使您能够获取通常无法从客户端应用程序的 Web UI 访问的敏感用户数据
由于 HTTPOnly
属性通常用于会话 cookie,攻击者通常也无法通过 XSS 直接访问它们。然而,通过窃取 OAuth 代码或令牌,攻击者可以在自己的浏览器中访问用户的账户。这给了他们更多的时间来探索用户数据并执行有害操作,从而显著增加了 XSS 漏洞的严重性
在无法注入 JavaScript 的情况下(例如,由于 CSP 限制或严格的过滤),您仍然可能能够使用简单的 HTML 注入来窃取授权码。如果您可以将 redirect_uri
参数指向一个可以注入您自己的 HTML 内容的页面,您可能能够通过 Referer
头泄露代码。例如,考虑以下 img
元素: <img src="/2025/05/02/undefined.htmlundefined" >
。当尝试获取此图像时,一些浏览器(如 Firefox)将在请求的 Referer
头中发送完整的 URL,包括查询字符串
范围升级:授权码流程
例如,假设攻击者的恶意客户端应用程序最初请求使用 openid email
范围访问用户的电子邮件地址。在用户批准此请求后,恶意客户端应用程序接收授权码。由于攻击者控制自己的客户端应用程序,他们可以在代码/令牌交换请求中添加另一个 scope
参数,包含额外的 profile
范围:
POST /token Host: oauth-authorization-server.com … client_id=12345&client_secret=SECRET&redirect_uri=https://client-app.com/callback&grant_type=authorization_code&code=a1b2c3d4e5f6g7h8&scope=openid%20 email%20profile
如果服务器不对此与初始授权请求的作用域进行验证,它有时会使用新的作用域生成访问令牌,并将其发送到攻击者的客户端应用程序:
{ "access_token": "z0y9x8w7v6u5", "token_type": "Bearer", "expires_in": 3600, "scope": "openid email profile", … }
query
和fragment
在OAuth协议中,response_mode
参数的query
和fragment
是两种不同的响应模式,主要用于控制授权码或令牌返回给客户端的方式。以下是两者的核心区别:
1. 参数传递位置
query
模式
将参数附加到重定向URI的查询字符串中,例如:https://example.com/callback?code=xxx&state=xxx
适用场景:通常与response_type=code
(授权码模式)配合使用,适用于需要将临时授权码传递回客户端的场景
安全性:存在风险,因为授权码可能暴露在URL中,若被恶意记录(如服务器日志),可能导致令牌泄露
fragment
模式
将参数放在URL的片段标识符(#
后的部分)中,例如:https://example.com/callback#access_token=xxx&token_type=Bearer
适用场景response_type=id_token或token
配合使用,适用于直接返回身份令牌或访问令牌的场景
安全性:更安全,因为浏览器不会将片段部分发送到服务器,且令牌不会出现在服务器日志中
2. 使用限制
query
模式仅支持授权码:OAuth规范建议query
模式仅用于传递授权码(code),因为令牌(access_token或id_token)若通过此模式返回,可能被恶意截获
- 单次使用:授权码只能使用一次,且需通过后端服务器与OAuth提供方交换令牌。
fragment
模式- 支持令牌直接返回:允许直接返回访问令牌或身份令牌,适用于隐式授权流程(如单页应用SPA)。
- 令牌可复用风险:若令牌通过片段返回,需确保客户端能安全存储(如浏览器本地存储),避免被恶意脚本窃取。
3. 典型应用场景
模式 | 适用场景 | OAuth提供方示例 |
---|---|---|
query | 需要后端服务器参与交换令牌的流程(如传统Web应用) | Apple、部分传统服务商 |
fragment | 前端直接处理令牌的场景(如SPA、移动端),避免后端介入 | Google、Facebook、Microsoft |
4. 安全建议
优先使用
fragment
模式:避免授权码或令牌暴露在URL查询参数中,降低中间人攻击风险。结合
state
参数:无论使用哪种模式,均需通过state参数验证请求来源,防止CSRF攻击
限制令牌作用域:即使通过
fragment
返回令牌,也应限制其权限范围和有效期。
总结
维度 | query | fragment |
---|---|---|
参数位置 | URL查询字符串(?key=value ) |
URL片段标识符(#key=value ) |
安全性 | 较低(可能泄露授权码) | 较高(片段不发送至服务器) |
适用类型 | 授权码(code ) |
令牌(token /id_token ) |
典型流程 | 传统Web应用(需后端交换令牌) | SPA/移动端(前端直接处理令牌) |
通过合理选择response_mode
,可优化OAuth流程的安全性和兼容性。