跨站请求伪造与 OAuth2

工程 | Dave Syer | 2011年11月30日 | ...

在这篇短文中,我们将结合 OAuth2 的背景,探讨跨站请求伪造,并分析在使用 OAuth2 保护 Web 资源时可能发生的攻击及其应对方法。

OAuth2 是一种协议,它允许客户端应用程序(通常是 Web 应用程序)在用户的许可下,代表用户执行操作。客户端允许执行的操作是在资源服务器(另一个 Web 应用程序或 Web 服务)上进行的,用户通过告知授权服务器信任客户端的请求操作来批准这些操作。互联网上常见的授权服务器示例包括 FacebookGoogle,它们也都提供资源服务器(Facebook 是 Graph API,Google 是 Google APIs)。

跨站请求伪造 (CSRF 或“sea surf”) 攻击涉及恶意攻击者诱使用户点击一个链接,该链接会更改目标系统上的某些状态。如果用户已登录到目标系统,由于浏览器会自动发送身份验证头或 cookie,他们可能甚至不会注意到攻击。

使用 OAuth2 保护资源和委托权限的系统无论如何都会受到所有“常规”CSRF 攻击的威胁——用户会进行身份验证,并且状态可能会被更改。在这里,我们将重点关注 OAuth2 协议特有的攻击,在这种情况下,恶意攻击者将试图获取访问令牌,该令牌将使他能够执行用户(在令牌范围内)可以执行的任何操作。

我们在下面讨论的一些 CSRF 防御措施来自对 OAuth2 客户端应用程序的精心实现,因此,只要授权服务器实现了 OAuth2 规范,任何客户端都可以使用它们。一些防御措施存在于 OAuth2 授权服务器本身,因此只能由服务器组件的开发人员实现。一些措施是规范强制执行或强烈建议的,而另一些则仅仅来自对协议的谨慎使用。

下面的一些 CSRF 攻击示例以内联链接的形式给出。点击它们是安全的——这只是一个演示,没有任何数据可以被损坏或身份信息可以被泄露。您可以使用登录屏幕上建议的凭据(marissa/koala)登录授权服务器。这些攻击之所以成功,仅仅是因为演示系统实现得很糟糕。您应该希望真实的 OAuth2 实现者会更加谨慎。相对于授权服务器而言,这可能是一个不过分的希望,但您不能如此确定客户端,并非因为这类应用程序有什么特别之处,而是因为客户端数量更多,并且来源更加多样。

对授权服务器的攻击

恶意攻击者试图通过诱使用户在用户已登录时点击授权服务器的链接来获取访问令牌。示例依赖于授权服务器可能重定向到任何 URI 的事实。除非为特定客户端预先注册了特定的重定向 URI,否则规范允许这样做。有了注册的重定向 URI,攻击就不会奏效,但这可能对某些系统来说过于严格,例如,Facebook 不会为此烦恼,但 Google 会。实际上,Facebook 会将重定向 URL 限制为“属于”一个预先注册的应用程序(即以相同的宿主、路径等开头),这相当明智,但并非规范的一部分。

要尝试这些攻击,您需要一个授权服务器,然后您可以简单地点击链接,查看重定向中暴露了哪些秘密。重定向中有一个简单的脚本,它捕获浏览器窗口的位置并将其镜像,以演示恶意攻击者如何提取他需要的信息并将其发送到别处。因此,如果您在浏览器中看到某些秘密信息,例如访问令牌,则攻击就成功了。这只是一个演示。真正的恶意攻击者将获取访问令牌并使用它来代表您(或在演示中代表 mariss'a)做坏事。该服务器应运行在 http://oademo.cloudfoundry.com - 如果它没有运行,那么实现就是来自 Spring Security OAuthsparklr2 应用程序,并且您可以通过更改示例中的链接来在本地运行它。

隐含授权攻击

隐含授权攻击可以说是最卑鄙的,因为恶意攻击者无需做任何额外工作即可获得令牌。但是,他确实需要知道客户端密钥以及有效的客户端 ID。

因此,此攻击成功。隐含授权:无密钥客户端 点击此处

但是此攻击失败。隐含授权:带密钥客户端: 点击此处

授权码攻击

这里我们先回顾一下授权码授予流程的基本流程,然后再讨论对其的攻击。

序列图,auth-code-flow

User->Client: GET /peek

participant AuthServer

activate User activate Client

Client->User: 302: location=auth/authorize deactivate User deactivate Client

User->AuthServer: GET /authorize activate User activate AuthServer AuthServer->User: {messages: "Do you approve?"} deactivate User deactivate AuthServer

User->AuthServer: approve activate User activate AuthServer AuthServer->User: 302: location=client/handle_code?code=dkshfjg deactivate User deactivate AuthServer

User->Client: GET /handle_code?code=dkshfjg activate User activate Client

Client->AuthServer: POST: /token?code=dkshfjg activate AuthServer AuthServer->Client: 200: {access_token:CNMBVCXKVY} deactivate AuthServer

Client->ResourceServer: GET /resource(access_token) activate ResourceServer ResourceServer->Client: 200: response deactivate ResourceServer

Client->User: 200: result deactivate Client deactivate User

授权码攻击允许恶意攻击者窃取授权码,然后该授权码可以兑换成令牌。恶意攻击者可以独立于客户端密钥获取授权码,但要使用它,他将需要密钥。因此,这两个链接都会返回一个有效的代码,但其中一个受客户端密钥保护,如果缺少密钥则无法用于冒充用户。

授权码授予:无密钥客户端 点击此处

授权码授予:带密钥客户端: 点击此处

序列图,auth-code-csrf

participant User

participant BadClient

User->AuthServer: GET /authorize?client_id=good&redirect_uri=/bad/peek&state=poioiu

note right of AuthServer AuthServer checks validity of state end note

activate User activate AuthServer

AuthServer->User: 302: location=/bad/peek?code=dkshfjg&state=poioiu deactivate User deactivate AuthServer

User->BadClient: GET /peek?code=dkshfjg&state=poioiu activate User activate BadClient

BadClient->AuthServer: POST: /token?code=dkshfjg&state=poioiu

activate AuthServer

note right of BadClient Assume BadClient knows client secret end note

AuthServer->BadClient: 200: {access_token:CNMBVCXKVY}

deactivate AuthServer deactivate BadClient deactivate User

对客户端的攻击

客户端应用程序也容易受到 CSRF 攻击,攻击目的不是窃取访问令牌,而是更改客户端或(更可能)客户端使用的资源服务器上的状态。在这种情况下,提供商系统无法阻止攻击,但它们可以帮助客户端实施自身的保护。主要的机制是客户端生成和管理的 state 参数,并由授权服务器完整地传递。

下面是带有 state 参数的完整授权码授予流程的序列图。客户端通过检查用户返回以获取访问令牌时,state 是否存在于用户的会话中来实现 CSRF 保护。此设计中的 state 参数是客户端应用程序中已登录用户会话中会话属性的密钥。

User->Client: GET /peek

participant AuthServer

activate User activate Client

note right of Client: generate random key for state Client->Session: store: {request:/peek, poioiu:XASFDAS} activate Session Client->User: 302: location=auth/authorize?state=poioiu deactivate User deactivate Client

User->AuthServer: GET /authorize?state=poioiu activate User activate AuthServer AuthServer->User: 302: location=client/handle_code?code=dkshfjg&state=poioiu deactivate User deactivate AuthServer

User->Client: GET /handle_code?code=dkshfjg&state=poioiu activate User activate Client

note left of Client: check state Client->Session: get poioiu Session->Client: XASFDAS note left of Client: OK (state exists) Client->AuthServer: POST: /token?code=dkshfjg activate AuthServer AuthServer->Client: 200: {access_token:CNMBVCXKVY} deactivate AuthServer

Client->Session: get request Session->Client: /peek note left of Client: continue with /peek Client->ResourceServer: GET /resource(access_token) activate ResourceServer ResourceServer->Client: 200: response deactivate ResourceServer

Client->User: 200: result deactivate Client deactivate User

如果 state 的密钥是可猜测的,那么此示例中的 CSRF 攻击仍然可能成功,但只要客户端在授予令牌后清理了会话,攻击就必须发生在具有相同密钥的现有合法授予期间。

这将是不幸的(并且不太可能),但 OAuth2 规范仍然建议客户端使用不可猜测的(例如,带有随机组件)state。规范不认为客户端本身是无状态的,这使得该建议更容易理解:如果 state 参数不仅仅是会话中的一个密钥,而是以某种不透明的方式编码了整个 state,那么使其不可猜测就更为重要。

防御措施

以下是可用于对抗上述攻击的可能防御措施的摘要。它们都可以使用 Spring SecuritySpring Security OAuth 构建的系统来实现,但许多并非自动强制执行——与开发者框架一贯的做法一样,开发者必须选择使用提供的功能。

对客户端

  • 使用客户端密钥
  • 使用注册的重定向 URI,可以是固定的,也可以是可变的(如果授权服务器允许,见下文)
  • 向授权端点发送随机的 state 参数值,并在用户会话中存储具有相同密钥的内容
  • 仅在收到授权码请求时,才向授权端点发送带有 state 参数的请求
  • 收到授权码请求时,在用户会话中检查 state
  • 如果客户端是无状态的(没有会话),则在 state 密钥本身中编码用户相关信息,并进行比较

对授权服务器

  • 始终使用 SSL,以防止机密信息和令牌被普通观察者嗅探
  • 要求客户端注册时提供密钥,并可能提供更强的密钥管理功能
  • 不要向动态注册的客户端公开客户端密码授予
  • 通过头而不是表单参数要求客户端身份验证。这使得诱使用户点击 CSRF 链接变得非常困难,因为他们需要一个脚本,而且同源策略可能会在普通浏览器中阻止该攻击。
  • 固定重定向:只重定向到客户端注册的固定 URL,或者
  • 可变重定向:允许用户重定向,但要求客户端注册一个网站地址,并强制所有重定向都托管在该地址

结论

我们已经探讨了一些针对 OAuth2 系统的 CSRF 攻击以及可以采取的一些防御措施。总的结论是,有很多机会可以击败此类攻击,其中一些源于规范,一些则不。与任何安全漏洞一样,系统是否能很好地防御 CSRF 取决于实现的细节以及密码和密钥的质量。即使是符合规范的系统也可能受到攻击,但通过谨慎的实现可以采取一些措施来使这些攻击不太可能成功。

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有