State-Of-The-Art OAuth2 Homakov Edition
考虑到官方OAuth2规范的脆弱性以及易受攻击的服务提供商和客户端的集成数,有了满足secure-by-default原则的OAuth版本-Homakov
CSRF防护初始请求/connect/provider的时候,总是使用和需要state参数,不允许动态的redirect_uri,默认不允许response_type=hash(以前是token),废弃refresh_token和token,而不是用appsecret_proof去认证每个access_token,仔细检查用户想要连接的服务提供商账户,access_token永不过期(把它当做公开的user_id)
1、用户点击连接服务提供商的按钮,无论是发起一个POST表单提交(受CSRF Token防护的)到/connect/provider_name,还是由GET导航到/connect/provider?csrf_token=TOKEN,初始步骤一定要受CSRF防护,它不应该被来自其他源的连接触发,必须有来自于用户的明确的确认。
2、在服务端/connect/provider客户端的控制器应该确认csrf_token和来自于session里的token一致,然后生成state token(CSRF Token,确保OAuth流程完整性),并命名为<provider>-state存储在session里。然后跳转到provider.com/oauth/authorize?client_id=CLIENT_ID&redirect_uri=CLIENT/callback&state=STAT,scope参数是可选的(应该仅仅允许被认证过的客户端接触一些关键的作用域)
3、服务提供商检查redirect_uri是否在重定向URI的白名单中(主机,路径,查询参数,hash必须是静态的)。每一个redirect_uri它不能是一个参数,还必须要有响应的返回类型,默认是query,移动app是hash。state参数必须由服务供应商要求(尽管规范上说该参数是可选的—对大多数开发者来说这就是不需要的代名词)。
4、用户在服务提供商域下看到同意/拒绝按钮,在点击同意按钮后受CSRF Token防护的POST表单被提交到相同的URL(不要像Outlook或Doorkeeper或者在授权的时候CSRF攻击服务提供商)。
5、response_type为query时候重定向到CLIENT/callback?access_token=TOKEN&state=STATE(所有的token被存储在中央服务器,并且每个API请求用appsecret_proof签名),response_type为hash的时候会重定向到CLIENT/callback#access_token=TOKEN&state=STATE(所有的token被存储在本地应用里,并不需要appsecret_proof)。这两种情况下客户端都一定要检查STATE是否等于来自season的provider-state。
6、为什么access_token从不过期?因为我们不再需要refresh_token/code。用refresh_token攻击者能使用access_token获得一个窗口去发送垃圾邮件或者下载所有受害者的社交图片。仅当access_token过期,根据规范,客户端应该使用refresh_token和client_secret去获取一个新的token。除了这个场景,我们将需要最早由Facebook引入的appsecret_proof。如果response_type为query的时候,token被返回,那么每一个到服务提供商的请求一定要有access_token和appsecret_proof=hash_hmac('sha256', $access_token, $client_secret);。现在如果攻击者泄露access_token,token毫无价值。即使他们偷了你的client_secret,你也能很简单的换掉。
7、可选的,但是是推荐的。在连接账户之前,你应该询问用户是否要连接到例如myfbemail@email.com的Facebook账户。因为有很多的攻击比如cookie forcing, login/logout CSRF, RECONNECT能够静默将用户登录到服务提供商的恶意账户里。
关于
这个文档描述常见的OAuth/Single Sign On/OpenID相关的漏洞。很多跨站交互容易受到不同类型的泄露和劫持。
Hacker和开发者都能通过阅读它收获益处。
OAuth是一个重要的功能,他负责对访问用户敏感数据进行认证和授权。糟糕的实现OAuth是一个接管账户的可靠方式,不像XSS,它很容易利用,但是对受害者来说很难避免(NoScript不会有作用,因为这个攻击不需要JavaScript)。
由于OAuth,许多初创公司包括SoundCloud,Foursquare,Airbnb,About.me、Bit.ly、Pinterest、Digg,StumbleUpon,Songkick有帐户劫持漏洞。并且很多网站仍然是脆弱的。我们的动机是让人们意识到“社交登录”的风险,我们鼓励你非常小心地使用OAuth。
这个小抄并不会解释OAuth是怎样的工作流程,请自行去官方站点查看
授权code流
通过连接攻击者提供的服务提供商账户导致客户端账户被劫持
正如最常见的OAuth2漏洞里面说的,换句话说就是CSRF。
服务提供商通过重定向用户代理(译者注:浏览器)到SITE/oauth/callback?code=CODE来返回授权code,现在客户端必须发送授权code和客户端凭证、redirect_uri一起来获得access_token
如果客户端执行的时候没有使用state参数去减轻CSRF问题,我们可以很容易的使用我们的服务提供商账户连接受害者的客户端账户。

它对有社交登录和有能力为已存在的主账户添加一个登陆选项的客户端有用(下面是来自pinterest.com的截图)


修复:在发送用户跳转到服务提供商的请求时生成一个随机值并且保存在cookie或者session里面,当用户从服务提供商返回的时候确认你收到的state参数值和之前保存在cookies或session里面的那个值相等。
缺陷:由于以前的代码利用用户提供的/connect?state=user_supplied而不是生成一个随机的,所以在omniauth这种情况下是有可能控制固定state的。
这是另外一个OAuth设计问题 - 有时候开发者想要使用state表达出自己的目的,尽管你能同时发送这样的值state=welcome_landing.random_nonce但是毫无疑问它看起来很丑陋,一个简洁的方案是使用JSON Web Token作为state。
服务提供商乱用固定会话导致客户端账户劫持
尽管客户端合理的校验了state参数,我们仍然可能在服务提供商替换认证cookie为攻击者的账户:在登录(VK, Facebook)时候使用CSRF,头部注入,或者cookie forcing or tossing。
然后我们仅仅载入一个GET请求触发连接(omniauth中/user/auth/facebook),Facebook将返回攻击者的用户信息(uid=攻击者的uid)并且它最终将连接攻击者的服务提供商账户到受害者的客户端账户。
修复:确保添加一个新的社交连接需要一个有效的csrf_token,因为这是不可能触发CSRF的过程。理想情况下,使用POST而不是GET。
Facebook拒绝从他们那边修复登录时的CSRF,所以很多库仍然易受攻击。不要期望服务提供商给你可靠的认证数据。
泄露授权code导致账户劫持
OAuth文档很清楚的指明了服务提供商必须检查第一次的redirect_uri等于客户端用来获取access_token的redirect_uri。我们并没有真正的检查它因为它看起来很难出错。令人惊讶的是很多服务提供商错了:Foursquare (reported), VK (report, in Russian), Github (可能泄露私有项目的tokens), 和很多“自研”的单点登录插件。
攻击方式很直接:在客户端域内找到一个漏洞页面,插入跨域图片或者一个链接到你的网站,然后使用这个页面地址作为redirect_uri。当受害者载入构造好的URL,它将引导受害者到leaking_page?code=CODE,并且受害者的用户代理将授权code暴露在http头的Referrer字段。

现在你能重新使用泄露的授权code和实际的redirect_uri去登录受害者账户。
修复:灵活的redirect_uri是一个不好的做法,但是,如果你需要它,那么每次请求授权code的时候存储redirect_uri并且在access_token生成的时候校验它。
隐式流
在打开跳转链接的时候泄露access_token/signed_request
这是过去被媒体宣传为“重定向覆盖”的攻击,但事实上它已经被发现很多年了。你仅需要找到一个客户端域名或者其子域名的重定向漏洞,把该地址作为redirect_uri并且替换response_type为token,signed_request发送出去,302跳转将会保留#fragment,而攻击者的Javascript代码可以访问location.hash(即为url的fragment字段值)。
泄露access_token能够被用来发送垃圾邮件和破坏你的隐私。此外,泄露的signed_request是更敏感的数据。通过在你完全授权用Facebook登录的客户端找到一个重定向漏洞。
修复:在应用的设置里仅允许一个redirect_uri白名单。

使用攻击者的客户端发出的access_token导致账户劫持
也被称为One Token to Rule Them All,这个bug与移动端和客户端应用有关,因为他们经常直接使用用户提供的access_token。
想象一下,用户有很多“授权秘钥”并且给任何他想要登录的新网站一个秘钥。一个恶意的站点管理员能够使用用户的的秘钥去登陆他的客户已经登录过的其他站点。

修复: 在接受用户提供的access_token前检查下它是否是你的client_id在https://graph.facebook.com/app?fields=id&access_token=TOKEN生成的。
传输和JS SDK漏洞
为了更好的支持客户端应用(或者浏览器应用),一些OAuth2服务提供商添加了一些隐式流外的自定义规格变体。涉及到一个代理/中继页作为访问token的路由。这个代理和客户端应用的页面通讯或者通过标准的HTML5的postMessage API,或者使用旧的方式:指定window name和url(也被叫做Fragment传输),还有Adobe Flash的LocalConnection API
不管选择的方法是什么,确保代理呢个个仅和目标源交换信息是很重要的,不接受来自伪造的应用的页面,并且最终去确认这些客户端的检查和服务端的检查是一致的。客户端安全传输失败通常导致下面两种问题:
- 泄露数据给攻击者(授权和认证绕过)
- 信任来自攻击者的数据(session固话攻击)
另外,现代web应用趋向于丰富的javascript前端库,以及不安全的客户端通讯导致超越OAuth本身的更严重的攻击。
修复:
- 完全禁用所有旧的客户端通讯方法(
Flash或者Fragment),使用postMessage - 检查所有进和出的消息的源,仅允许目标应用的域
- 确保所有客户端源检查和服务端源检查一致
- 在与另一方开始数据传输前用加密的随机数校验
其他
##泄露客户端凭证威胁
客户端凭证不再像它听起来那样重要。你唯一能做的是使用泄露的页面泄露授权code,然后手动为它们获取access_token(提供泄露的redirect_uri,而不是实际的)。尽管这个威胁当服务提供商使用静态redirect_uri的时候能够得到缓解。
session固话(OAuth1.0)
OAuth2和OAuth1最主要的不同在于你传给服务提供商的参数。第一个版本里你发送所有的参数给服务提供商然后获取对应request_token。接着你引导用户到provider?request_token=TOKEN,然后在授权用户完后重定向返回到client/callback?request_token=SAME_TOKEN。
在这里固话的意思是我们能欺骗用户接受我们提供的发送给我们的Token1,然后在客户端的callback里重用Token1
这个不是严重的漏洞,因为主要是基于网络钓鱼。FYI Paypal express checkout has this bug
中间服务提供商
许多初创公司都用Facebook连接,与此同时,他们也是服务提供商。作为服务提供商,他们必须重定向用户到第三方站点,这些“开放的重定向”你没法修复。它使得这个链成为可能:Facebook -> 中间服务提供商 -> 客户端的回调,导致FB的token泄露。
要修复这个问题,Facebook在回调URL的后面添加#_=_。你调用的时候应该去掉fragment来防止泄露。以这种方式重定向:Location: YOUR_CLIENT/callback?code=code#
用技巧绕过redirect_uri校验
如果你被允许设置子目录,下面是目录遍历的技巧:
- /old/path/../../new/path
- /old/path/%2e%2e/%2e%2e/new/path
- /old/path/%252e%252e/%252e%252e/new/path
- /new/path///../../old/path/
- /old/path/.%0a./.%0d./new/path (For Rails, because it strips \n\d\0)
重放攻击
授权code通过GET发送,并将存储在日志中,服务提供商一定要在使用或过期5分钟内删除它。
开放的重定向泄露授权code
通常你需要referrer泄露页面去泄露查询参数。这有两种通过开放的重定向来实现的技巧:
- 当重定向使用
<meta>标签而不是302状态和Location头的话,它将泄露跳转页面的referrer到下一个请求中。 - 当你管理
redirect_uri的尾部去添加%23(#),它将导致在fragment位置发送授权codeLocation: http://CLIENT/callback/../open_redirect?to=evil#&code=CODE
贡献者
@homakov(https://twitter.com/homakov), @isciurus(https://twitter.com/isciurus) and you?