Cookies 的跨域脚本攻击 - Github 迁移域名的安全详解
| 2013-04-14 22:54 收藏: 1
上周五我们宣布并完成了把所有的GitHub页面迁移到新域名github.io。 这是个计划已久的行动,此举是为了防止恶意网站攻击和跨域cookie的漏洞,这些漏洞是通过在我们主站的子域名下控制客户内容产生的。
关于这些跨域攻击漏洞的可怕影响,大家可能会有一些困惑。我们希望这篇技术博客可以消除这些困惑。
一个二级域名传过来的Cookie
当你登陆GitHub.com时,我们通过响应的HTTP头部来设置session的cookie。这个cookie包含着唯一标识你的session数据:
Set-Cookie: _session=THIS_IS_A_SESSION_TOKEN; path=/; expires=Sun, 01-Jan-2023 00:00:00 GMT; secure; HttpOnly
这些GitHub发送给浏览器的session的cookie是设定在默认的域名上(github.com),这就意味着这些cookie是不能从二级域名*.github.com访问到的。而且我们也指定了HttpOnly属性,这意味着cookie也不能通过JavaScript 的API:document.cookie来读取。最后,我们指定了Secure属性,这意味着这些cookie只能通过HTTPS来传输。
因此,从GitHub托管网站读取或"窃取"session的cookie是不太可能的。通过在GitHub网站托管的用户代码是不容易获取到session的cookie,但由于浏览器通过HTTP请求来发送cookie,这种方式有可能把cookie从GitHub网站抛到GitHub父域名上。
当浏览器执行一个HTTP请求时,它通过header里单独的cookie发送一些和URL匹配的cookie,这些发送的cookie是以键-值对存在的。只有和请求的URL匹配的cookie才会发送出去,比如,当执行一个对github.com的请求时,设置在域名github.io上的cookie是不会发送的,但在github.com上的cookie将会发送。
GET / HTTP/1.1 Host: github.com Cookie: logged_in=yes; _session=THIS_IS_A_SESSION_TOKEN;
Cookie抛出的问题是因为header中的cookie只包含了一系列键值对的cookie,并没有一些其他信息, 通过这些额外信息可以知道cookie设置在哪个域名上,比如路径或者域名。
最直接的跨域攻击涉及到:在GitHub托管网站页面,通过document.cookie这个JavaScript API设置一个_session的cookie。假设这个网站托管在*.github.com,那么这个cookie将会被设置到父域名的所有请求里,尽管事实是它只设置在了二级域名里。
/* set a cookie in the .github.com subdomain */ document.cookie = "_session=EVIL_SESSION_TOKEN; Path=/; Domain=.github.com"
GET / HTTP/1.1 Cookie: logged_in=yes; _session=EVIL_SESSION_TOKEN; _session=THIS_IS_A_SESSION_TOKEN; Host: github.com
在这个示例中,通过JavaScript在二级域名上设置的cookie被发送旁边合法的cookie字段中,并设置到父域名里。如果域名,路径,Secure和HttpOnly属性未设置的话,根本 没有方法去判断哪个cookie来自哪里。
这对大部分web服务器来说是一个大问题,因为在一个域及其子域中的cookies的顺序并不是有RFC6265指定的,并且web浏览器可以选择以任何顺序发送它们。
对于Rack--为Rails和Sinatra提供动力的web服务器界面,包括其他的,cookis解析如下:
def cookies hash = {} cookies = Utils.parse_query(cookie_header, ';,') cookies.each { |k,v| hash[k] = Array === v ? v.first : v } hash end
如果在Cookie:header里有不止一个有着相同名字的cookie时,第一个cookie将会被假定成任意值。
这是一个很显而易见的攻击:几周之前,安全专家Egor Homakov在博客中就用这个方法证明了该攻击确实存在.这个漏洞的影响是不严重的(每次登录后,跨站点伪造请求的令牌会被重置,所以它们不会一直固定不变),但这是个非常实际的例子,人们可以很容易伪造注销用户,令人很郁闷.这使得我们必须尽快完成把GitHub页面迁移的他们自己的域名,但只留给我们几周的时间(到迁移完成之前),在这期间我们必须减轻已知的攻击数量.
幸运的是,已知的攻击在服务端很容易减轻.我们预想到会有一些其他的攻击,这些攻击或者很难处理,或者根本不可能存在.那么让我们一起看看这些它们.
免受cookie抛出的伤害
第一步是减轻cookie抛出造成的攻击.这个攻击暴露出浏览器将会发送2个相同名字的cookie令牌,不让我们知道它们是设置在哪个域名上的.
我们没法判断每个cookie是来自哪里的,但如果我们跳过cookie的解析,我们就能看出每个请求是否包含2个相同的_session的cookie.这个极有可能是由于有些人从二级尝试域名抛出这些cookie,所以我们不是猜测哪个cookie是合法的,哪个是被抛过来的,而是简单地通知浏览器在继续执行之前放弃二级域名上设置的cookie.
为了完成这个示例,我们创造了一个特殊的响应:我们让浏览器跳转到刚刚请求的URL,但带着一个Set-Cookie的header,这个header放弃了二级域名上的cookie.
GET /libgit2/libgit2 HTTP/1.1 Host: github.com Cookie: logged_in=yes; _session=EVIL_SESSION_TOKEN; _session=THIS_IS_A_SESSION_TOKEN;
HTTP/1.1 302 Found Location: /libgit2/libgit2 Content-Type: text/html Set-Cookie: _session=; Expires=Thu, 01-Jan-1970 00:00:01 GMT; Path=/; Domain=.github.com;
我们决定按照Rack中间件来实现这个功能.这种方式在 应用运行之前可以执行检查cookie和顺序重定向的工作.
当触发Rack的中间件时,在用户不会意识到的情况下,这个重定向会自动发生,并且第二个请求将只会包含一个_session的cookie:合法的那一个.
这个"破解"足够减缓大部分人所遇到的直接抛出cookie的攻击,但还有一些更复杂的攻击也需要我们思考一下.
Cookie路径方案
如果一个恶意的cookie设置到一个具体的路径,这个路径不是根路径(例如,/notifications),当用户访问github.com/notifications时,浏览器会发送那个cookie,当我们在根路径上清除这个cookie时,我们的header不会起作用.
document.cookie = "_session=EVIL_SESSION_TOKEN; Path=/notifications; Domain=.github.com"
GET /notifications HTTP/1.1 Host: github.com Cookie: logged_in=yes; _session=EVIL_SESSION_TOKEN; _session=THIS_IS_A_SESSION_TOKEN;
HTTP/1.1 302 Found Location: /notifications Content-Type: text/html # This header has no effect; the _session cookie was set # with `Path=/notifications` and won't be cleared by this, # causing an infinite redirect loop Set-Cookie: _session=; Expires=Thu, 01-Jan-1970 00:00:01 GMT; Path=/; Domain=.github.com;
这个方案非常直截了当,虽然不太雅:对于任何指定的请求URL,如果其路径部分匹配请求的URL,浏览器将只会发送一个恶意的JavaScript cookie.所以我们只需要在每个路径的元素上放弃这个cookie就可以了.
HTTP/1.1 302 Found Location: /libgit2/libgit2/pull/1457 Content-Type: text/html Set-Cookie: _session=; Expires=Thu, 01-Jan-1970 00:00:01 GMT; Path=/; Domain=.github.com; Set-Cookie: _session=; Expires=Thu, 01-Jan-1970 00:00:01 GMT; Path=/libgit2; Domain=.github.com; Set-Cookie: _session=; Expires=Thu, 01-Jan-1970 00:00:01 GMT; Path=/libgit2/libgit2; Domain=.github.com; Set-Cookie: _session=; Expires=Thu, 01-Jan-1970 00:00:01 GMT; Path=/libgit2/libgit2/pull; Domain=.github.com; Set-Cookie: _session=; Expires=Thu, 01-Jan-1970 00:00:01 GMT; Path=/libgit2/libgit2/pull/1457; Domain=.github.com;
当谈到cookie时,我们需要在服务端做关联.我们唯一目的是用这个强力方式清楚那些cookie,这种方式虽然暴力,但完成github.io的迁移后,效果非常好.
Cookie溢出
让我们加强我们的游戏:另一种攻击将会执行,它利用RFC 6265没有明确指明一种cookie的溢出行为.大部分web服务器/接口,包括Rack,假定cookie的名字可以加密(如果他们包含不是ASCII的字符时,这就是个疯狂的假设),所以当生成cookie列表时不会溢出:
cookies = Utils.parse_query(string, ';,') { |s| Rack::Utils.unescape(s) rescue s }
这就允许一个恶意用户去设置一个cookie,这个cookie能被web框架理解成_session,尽管在浏览器里这个cookie的名字并不是_session.这个攻击会把没必要溢出的cookie字符溢出掉:
GET / HTTP/1.1 Host: github.com Cookie: logged_in=yes; _session=chocolate-cookie; _%73ession=bad-cookie;
{ "_session" : ["chocolate-cookie", "bad-cookie"] }
如果我们试着丢弃Rack产生的cookie列表中的第二个,我们的header就会失效.在Rack解析以后,我们会失去重要的信息:通过加密后的cookie名字和web框架接收到的名字将不一致.
# This header has no effect: the cookie in # the browser is actually named `_%73ession` Set-Cookie: _session=; Expires=Thu, 01-Jan-1970 00:00:01 GMT; Path=/; Domain=.github.com;
为了解决这个问题,我们必须跳过Rack的cookie解析,可以通过禁用不溢出,然后找到所有和我们目标匹配的那些cookie名字.
cookie_pairs = Rack::Utils.parse_query(cookies, ';,') { |s| s } cookie_pairs.each do |k, v| if k == '_session' && Array === v bad_cookies << k elsif k != '_session' && Rack::Utils.unescape(k) == '_session' bad_cookies << k end end
这种方式我们可以丢弃对的cookie(它或者设置成_session,或者作为溢出的偏差).在这种中间件的帮助下,我们可以解决所有的能在服务端解决的cookie抛出引起的攻击.不幸的是,我们意识到有另一种攻击会使得中间件的保护失效.
Cookie溢出
如果你遇到了cookie的问题,我为你惋惜. 我已经有了99个cookie,我的域名不能再增加一个了.
这是一个稍微更高级的攻击,它暴露出所有web浏览器对每个域名设置的cookie的数量限制.
比如,火狐设置的数量限制是150,谷歌浏览器设置的是180.问题是这个限制不是在每个cookie的域名属性上设置的,而是通过cookie设在的实际域名来定义的.一个单独的HTTP请求访问主域名和子域名上的任一页面,将会发送最大数量的cookie,但哪些cookie被使用的规则确实没有定义的.
例如谷歌浏览器不会关心父域名上的那些cookie,这些cookie是通过HTTP设置或者用Secure设置的:它将会发送180个新的cookie.这使得非常容易"剔除"每一个单独的从父域名过来的cookie,并用一些子域名上用JavaScript运行的假的cookie来替代它们:
for (i = 0; i < 180; i++) { document.cookie = "cookie" + i + "=chocolate-chips; Path=/; Domain=.github.com" }
在子域名上设置了180个这样的cookie之后,所有从父域名过来的cookie就消失了.如果现在我们终止我们刚刚设置的cookie,也包含JavaScript那部分,那么子域名和父域名上的cookie列表就会变空:
for (i = 0; i < 180; i++) { document.cookie = "cookie" + i + "=chocolate-chips; Path=/; Domain=.github.com; Expires=Thu, 01-Jan-1970 00:00:01 GMT;" } /* all cookies are gone now; plant the evil one */ document.cookie = "_session=EVIL_SESSION_TOKEN; Path=/; Domain=.github.com"
这允许我们执行一个只带有一个_session的cookie的单独的请求:这个cookie是我们用JavaScript创造的.原来的Secure和HttpOnly在_session的cookie里没有了,并且没有方法在web服务端检测发送的cookie既不是Secure,HttpOnly,也不是在父域名中设置的,但是完全虚构的.
在服务端只设置一个_session的cookie,就没有方法知道cookie是否被抛出了.即使我们发现了一个不合法的cookie,这样的攻击也仅能把用户从GitHub注销.
结论
正如我们看到的,通过在浏览器溢出cookie,我们可以制造带有恶意cookie的请求,这些请求不能在服务端阻隔.这里没有什么新的知识:Egnor的原始概念攻击的证据和暴露在这里的变种攻击都早已被世人所知.
现在看起来,在子域名上控制用户自定义的内容是一种安全的自杀行为,尤其在谷歌浏览器当前实现方案下,更加剧了这种自杀行为.火狐处理父域和子域上cookie区别的方式更优雅(用更一致的排序来发送它们,并且分离它们的存储来防止子域的溢出),谷歌浏览器就没有这种区别,并且对通过JavaScript设置的cookie和服务器通过Secure,HttpOnly设置的cookie一视同仁,导致一个非常完美的抛出攻击.
不管如何,通过HTTP header来传输cookie的行为是模糊的和依赖于实现的,迟早会有人提出另一种跨域抛出cookie的方式,和目标浏览器无关.
当cookie抛出的攻击并不是太危险的(比如,拦截用户session,或者实行网络欺诈/骚扰用户是不太可能的),它们会直截了当的进行,这非常使人恼火.
我们希望这篇文章能帮助大家提高这些攻击问题的防范意识和不通过迁移域名来防止这些攻击的困难点,所以迁移域名是一个激进的但最终必须的措施.
via https://github.com/blog/1466-yummy-cookies-across-domains
via http://www.oschina.net/translate/yummy-cookies-across-domains