OAuth2 跨站请求伪造和状态参数

信息安全 csrf oauth 授权
2021-08-30 14:03:28

https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-30#section-10.12说:

客户端必须实现 CSRF 保护 [...] 通常通过要求发送到重定向 URI 端点的任何请求包含将请求绑定到用户代理的身份验证状态的值(例如会话 cookie 的哈希 [... ]

不过,它并没有说太多关于实现的内容。它只是说明了 CSRF 的工作原理:

针对客户端重定向 URI 的 CSRF 攻击允许攻击者注入他们自己的授权代码或访问令牌,这可能导致客户端使用与攻击者的受保护资源相关联的访问令牌,而不是受害者的(例如,将受害者的银行帐户信息保存到受攻击者控制的受保护资源)

但是使用“相当”这个词反而会使声明变得毫无价值。

我正在考虑如何在 GAE 中实现“状态”(使用 Webapp2)。从黑客如何使用 CSRF 对抗 OAuth2 开始,这将是最简单的。我只找到了一篇关于此事的好文章:“Cross Site Request Forgery and OAuth2”

不幸的是,虽然这篇博文写得很好,但除了解释 OAuth2 之外,没有太多信息。这些例子不起作用,我不知道Spring。尽管如此,我还是在那里发现了一个有趣的建议:连接到 OAuth2 提供程序的服务器应该将“状态”存储为随机会话密钥(例如“this_is_the_random_state”:“this_doesn't_matter”),而不是静态密钥下的值(例如“状态”:“随机状态字符串”)。


我的问题是,“状态”的合理实施是什么?

  • 应该对随机生成的状态进行散列处理,还是可以存储相同的值并将其发送给 OAuth2 提供者?
  • 如果会话后端是安全 cookie 或服务器端存储技术(例如在 GAE Memcache 或数据库中),这里有什么区别吗?
  • 是否应该按照建议将状态存储为密钥?
  • 状态是否应该有有效期,或者会话(如果有的话)生命周期是否足够?
4个回答

让我们来看看这种攻击是如何工作的。

攻击

  1. 我访问了某个客户的网站并开始授权该客户使用 OAuth访问某个服务提供商的过程

  2. 客户请求服务提供商代表我请求访问权限,该权限被授予

  3. 我被重定向到服务提供商的网站,我通常会在其中输入我的用户名/密码以授权访问

  4. 相反,我捕获/阻止此请求并保存其 URL

  5. 我以某种方式让您访问该 URL。如果您使用自己的帐户登录服务提供商,那么您的凭据将用于颁发授权码

  6. 授权码被交换为访问令牌

  7. 现在在客户端上的帐户被授权访问在服务提供商上的帐户

那么,我们如何使用state参数来防止这种情况呢?

预防

  1. 客户端应该创建一个基于原始用户帐户的值(例如,用户会话密钥的哈希)。它是什么并不重要,只要它是唯一的并且是使用有关原始用户的一些私人信息生成的。

  2. 该值在上述第三步的重定向中传递给服务提供者

  3. 现在,我让你访问我保存的 URL(上面的第五步)

  4. 授权码发出,并发送回客户端会话与沿state参数

  5. state客户端根据您的会话信息生成一个值,并将其与state从授权请求发送回服务提供商的值进行比较。此值与请求上的参数不匹配state,因为该state值是根据我的会话信息生成的,因此被拒绝。

你的问题

  • 应该对随机生成的状态进行散列还是可以存储相同的值并将其发送给 OAuth2 提供者?

关键是攻击者不应该能够生成特定于给定用户的状态值。应该是猜不透的。

  • 如果会话后端是安全 cookie 或服务器端存储(在 GAE Memcache 或数据库中),这里有区别吗?

我不认为这很重要(如果我理解正确的话)

  • 是否应该按照建议将状态存储为密钥?

我不知道这是什么意思。

  • 状态应该有有效期,还是会话(如果有)生命周期就足够了?

是的,状态应该有期限。它不一定必须与会话绑定,但可以。

我将简化这个问题。 跨站点请求伪造点击劫持攻击很有用,因为它可以强制受害者的浏览器执行违背其意愿的操作。

OAuth v2 RFC中提及10.12. Cross-Site Request Forgery具有基本相同的关注点。如果攻击者可以强制受害者的浏览器进行身份验证,那么这是强制受害者浏览器执行其他操作的有用步骤。10.13. Clickjacking

   in a clickjacking attack, an attacker registers a legitimate client
   and then constructs a malicious site in which it loads the
   authorization server's authorization endpoint web page in a
   transparent iframe overlaid on top of a set of dummy buttons, which
   are carefully constructed to be placed directly under important
   buttons on the authorization page.  When an end-user clicks a
   misleading visible button, the end-user is actually clicking an
   invisible button on the authorization page (such as an "Authorize"
   button).  This allows an attacker to trick a resource owner into
   granting its client access without their knowledge.

资料来源:10.13。点击劫持

例如,Stack Overflow 使用 OAuth 并且容易受到这种攻击。如果您访问 StackOverflow 并且您当前已登录到您的 OAuth 提供商,您将自动登录到 StackOverflow。因此,攻击者可以通过在 iframe 中加载 Stack Oveflow 来自动登录受害者。如果 Stack Overflow 也有一个 CSRF 漏洞(而且它确实有! ),那么攻击者可以自动验证受害者的浏览器并在链式攻击中对stackoverflow.com执行 CSRF(会话骑乘)、点击劫持或 XSS攻击

取自OAuth2 中状态参数的重要性

这就是 OAuth 2 中的“状态”对象发挥作用的地方。通过在 POST 到授权端点时始终提交不可猜测的状态,客户端应用程序可以确定从授权服务器获得的访问代码是响应它发出的请求,而不是响应其他一些客户端应用程序。

例子:

https://example.com/as/authorization?client_id=client1&response_type=code&scope=openid &state=7tvPJiv8StrAqo9IQE9xsJaDso4

为了使 state 参数在防止此类 CSRF 攻击中有用,向 OAuth 服务器发出的所有请求都必须包含客户端可以用来验证自身的 state 参数。在发送状态参数时,OAuth 规范规定授权服务器必须将其逐字返回给客户端。这将通过将其附加到客户端的回调 URL 来完成。客户端必须接收此状态并被编程为仅接受具有可验证状态的重定向。如果这是保存在内存中的字典或可重新计算的值,则取决于客户端程序员。

在决定如何实现这一点时,一个建议是使用私钥和一些易于验证的变量(例如客户端 ID 和会话 cookie)来计算散列值。这将导致一个字节值在没有私钥的情况下难以猜测。在计算出这样一个 HMAC 后,base-64 对其进行编码并将其作为状态参数传递给 OAuth 服务器。另一个建议是散列当前日期和时间。这需要您的应用程序保存传输时间,以便对其进行验证或允许有效的滑动期(例如,使用 TOTP)。

Hers 是一个简单的 Python 示例,它使用日期时间的第二个建议:

def generate_state_parameter(client_id, private_key):
    date = datetime.datetime.today()
    raw_state = str(date) + client_id
    hashed = hmac.new(private_key, raw_state, sha1) state = base64.b64encode(hashed.digest())
    return (state, date)

除了@wayne 的回答,这里还有另一种可能的攻击(CSRF 可以防止这种攻击):

  1. 攻击者获取自己账户的 OAuth 授权码;
  2. 攻击者使用攻击者的授权码诱骗用户进入重定向链接;
  3. 用户登录到应用程序,认为这是他们自己的帐户,而实际上它是攻击者的帐户。

例如,如果有问题的应用程序显示了应将资金转入的帐户的银行详细信息,那么这可能是一个主要问题。

CSRF 会阻止这种情况,因为 CSRF 代码将与攻击者的会话相关联,而不是与用户的会话相关联。因此应用程序会拒绝用户的身份验证,因为 CSRF 代码与用户的会话不匹配。