纯牛奶645
纯牛奶645
  • 发布:2018-06-01 09:42
  • 更新:2018-06-01 09:42
  • 阅读:5749

深入浏览器cookie,安全与共享

分类:HTML5+

我们都知道,HTTP是无状态的,但我们的服务器有时需要有区别的对待不同的客服端,如何区分,其实就是一种状态的表现,Cookie应运而生,当然,本文主题并不是介绍Cookie的历史和作用。
cookie安全
最近和同事在写管理系统前端时,想到用测试机上的数据来测试本地代码,这样我们在本地也能非常友好的去复现测试机上的bug,而且代码还是非混淆,非压缩的。

之前其实内部是有相关的代理工具,基本原理是通过将测试机的的ip指向本地(在自己机器的host上配置),然后本地配置nginx server,将静态资源请求转向本地的dev server,接口请求转向测试机。但我们觉得,既然本地起来一个dev的server,我们直接给这个server配置转发,书写规则,让其转发到测试机上对应的地址即可。

理想很丰满,现实很骨感:

服务器上对应接口永远返回的是401(未授权),好像确实少了某些操作。

服务器如何识别客服端,我想有过服务端开发经验的小伙伴不会陌生,对,就是session,服务端的一种状态化解决方案,当然,session的实现需要客服端cookie的配合,因为session id会通过cookie的方式下发到客服端,后续的请求都会带上这个session id,然后服务端取出存储的用户状态。基于这一点,我们将Cookie分为会话Cookie和持久性Cookie。
Session cookies
会话性质的Cookie,不会指定Expires或者Max-Age,当浏览器被关闭的时候将会被删除。当我们看到浏览器Cookie面板中Cookie的Expires / Max-Age为1969-12-31T23:59:59.000Z,也就是计算机时间0的前一秒时(Chrome中),我们差不多就可以认为这是个session cookie了。
Permanent cookies
持久性Cookie,可通过Expires/Max-Age来设置Cookies的过期时间。
回到问题
那么回到我们上面的问题,直接访问本地开的dev server是肯定带不过去Cookie的,因为localhost.com或者127.0.0.1 和 test.somsite.com/contextName之间从domain方面来说是压根没有半毛线关系的。
那如果我们对local.test.somsite.com配置host,然后指向127.0.0.1呢?访问local.test.somsite.com/contextName,会发现有一部分cookie是带过去了的,唯独我们的session cookie没有被带上。

**注:**此处有朋友可能会问,为毛不直接将host设置为test.somsite.com,一了百了,其实不是不可以,只是如果和测试机设置一样的hostname的话,我们可能要不断在这两个host之间switch了。
为毛带不过去,谁定义的规则,在rfc6265中,我想我们能够找到答案。这里多说一句,在rfc6265之前,其实是还有一个rfc 2109的。然后,你懂得,在一些旧版本的浏览器,或者一些后端技术中,依然会去尊重RFC 2109的一些规则。博主也会在后面提到我所遇到的一些和这相关的。
host-only-flag in Cookie
在rfc6265的5.3节(Storage Model),定义了浏览器在接收到服务端的Set-Cookie之后的一个存储机制,其实浏览器针对服务端设置的每一个Cookie,都会存储如下字段: name, value, expiry-time, domain, path, creation-time, last-access-time, persistent-flag, host-only-flag, secure-only-flag, and http-only-flag.当然,这并不是要求服务端设置这么多个字段,而是有默认值的,这个缺省值,就是我们这儿要讨论的。我们定位到第六点:

 6.   If the domain-attribute is non-empty:  
  
       If the canonicalized request-host does not domain-match the  
       domain-attribute:  
  
          Ignore the cookie entirely and abort these steps.  
  
       Otherwise:  
  
          Set the cookie's host-only-flag to false.  
  
          Set the cookie's domain to the domain-attribute.  
  
    Otherwise:  
  
       Set the cookie's host-only-flag to true.  
  
       Set the cookie's domain to the canonicalized request-host.

dimain-attribute为空时,将host-only-flag标记为true。

而host-only-flag是干啥的呢?我们先不看它的定义,看浏览器时如何使用这个属性的,同样是rfc6265, 5.4节(The Cookie Header),我们可以看到User-Agent是如何去决定带Cookie的:

The user agent MUST use an algorithm equivalent to the following algorithm to compute the "cookie-string" from a cookie store and a request-uri: (也就是用户代理在发送请求时,在决定是否带上对应Cookie方面的一个决策算法,注意其语法: MUST)

1.  Let cookie-list be the set of cookies from the cookie store that  
       meets all of the following requirements:  
  
       *  Either:  
  
             The cookie's host-only-flag is true and the canonicalized  
             request-host is identical to the cookie's domain.  
  
          Or:  
  
             The cookie's host-only-flag is false and the canonicalized  
             request-host domain-matches the cookie's domain.  
  
       *  The request-uri's path path-matches the cookie's path.  
  
       *  If the cookie's secure-only-flag is true, then the request-  
          uri's scheme must denote a "secure" protocol (as defined by  
          the user agent).  
  
             NOTE: The notion of a "secure" protocol is not defined by  
             this document.  Typically, user agents consider a protocol  
             secure if the protocol makes use of transport-layer  
             security, such as SSL or TLS.  For example, most user  
             agents consider "https" to be a scheme that denotes a  
             secure protocol.  
  
       *  If the cookie's http-only-flag is true, then exclude the  
          cookie if the cookie-string is being generated for a "non-  
          HTTP" API (as defined by the user agent).

我们可以看到其中第一点的第一小点,当 host-only-flag 被设置为true是,请求的主机的域名就必须与cookie中设置的domain完全匹配,否则不会带上此Cookie。

回到我们的session-cookie, 我们看下这个JSESSIONID 的 Cookie是如何被设置的:


那我们上面的问题就很清晰了,没有设置cookie的domain属性,对于请求的cookie的携带使用强domain匹配规则,自然是像子域这种也不会被发送过去的,这也符合我们session id这样的高安全需求且独立操作的场景要求。

Cookie共享
说完安全谈共享(share),其实二者并不是独立的,而是共生。正是因为共享,我们才需要安全的规则去限制,Web所营造的一个共享的环境让人有机可趁,而随之而来的一些安全策略就是去确保我们的信息,个人隐私的安全。Cookie如此,Same-origin policy亦是如此。

那么说到Cookie的共享,官方并不是用源(origin)来进行描述,而是"third-party"这个词,第三方。而针对用户代理如何去对待第三方Cookie,rfc并未作出明确的规定。

User agents vary widely in their third-party cookie policies. This document grants user agents wide latitude to experiment with third-party cookie policies that balance the privacy and compatibility needs of their users. However, this document does not endorse any particular third-party cookie policy.

这里其实还需要补充一点的是,对于请求是否该携带Cookie,我们cookie的domain字段在set-cookie时就已经做了明确的限定了,当没有指定domain的时候,上面已经列出了相关的策略,那如果指定了domain呢(我们同样可以看到rfc6265的Domain Matching),有兴趣的读者可以点击前往看下,我这里简单介绍下细则:

一个字符串str如果满足匹配一个给定的domain的话,它至少应该满足下面条件中的一个:

强匹配,就是完全一样
或者同时满足下面的条件:
domain字段的值是这个str的后缀
str中最后一个不包含在domain值中的字符应该是个点(%x2E (".")),这个其实很好理解,domain: google.com and str: map.google.com => OK; domain: google.com and str: map.mgoogle.com => SAD
这个字符必须是个域名(主机名),而不能是IP地址。
读者需要注意的是,Domain match,也就是我们上面的这个匹配规则,只是浏览器在决定是否携带我们的Cookie项时诸多参考项中的一个,即必要不充分。浏览器还会参考诸如是否过期,以及Path-Match等项。

说完浏览器对于携带Cookie项的一个决策策略,其实整个流程(或者说是Cookie的生命周期)还有一部分没有涉及,就是一开始,我们设置Cookie的时候,此时,浏览器也会有一系列的约束。这在我们规范的5.3. Storage Model部分做了相关的说明,我们可以注意到其4,5,6点(第六点在本文的前面有部分提到):

4.   If the cookie-attribute-list contains an attribute with an  
    attribute-name of "Domain":  
  
       Let the domain-attribute be the attribute-value of the last  
       attribute in the cookie-attribute-list with an attribute-name  
       of "Domain".  
  
    Otherwise:  
  
       Let the domain-attribute be the empty string.  
  
5.   If the user agent is configured to reject "public suffixes" and  
    the domain-attribute is a public suffix:  
  
       If the domain-attribute is identical to the canonicalized  
       request-host:  
  
          Let the domain-attribute be the empty string.  
      Otherwise:  
  
          Ignore the cookie entirely and abort these steps.  
  
6.   If the domain-attribute is non-empty:  
  
       If the canonicalized request-host does not domain-match the  
       domain-attribute:  
  
          Ignore the cookie entirely and abort these steps.  
  
       Otherwise:  
  
          Set the cookie's host-only-flag to false.  
  
          Set the cookie's domain to the domain-attribute.  
  
    Otherwise:  
  
       Set the cookie's host-only-flag to true.  
  
       Set the cookie's domain to the canonicalized request-host.

总结来说,当浏览器决定是否存储Cookie项时,在Domain这方面的决策大致为:首先检验当前domain属性的设值是否命中了我们浏览器的公共前缀(public suffixes),如果命中且当前这个domain的值能够完全匹配当前请求的域名,将domain值重置为空,走我们前面提到的host-only-flag流程;如果命中但domain值不能匹配当前请求的域名,本Cookie项设置失败。前面的case还有一个前提是浏览器被配置为零容忍(不允许)public suffixes(is configured to reject "public suffixes")。如果上面步骤没凉的话,第六点当domain属性未被置空时,这里就会有一次的domain-match的检测,检测失败则设置失败呗。

Cookies use a separate definition of origins. A page can set a cookie for its own domain or any parent domain, as long as the parent domain is not a public suffix. Firefox and Chrome use the Public Suffix List to determine if a domain is a public suffix. Internet Explorer uses its own internal method to determine if a domain is a public suffix. The browser will make a cookie available to the given domain including any sub-domains, no matter which protocol (HTTP/HTTPS) or port is used. When you set a cookie, you can limit its availability using the Domain, Path, Secure and Http-Only flags. When you read a cookie, you cannot see from where it was set. Even if you use only secure https connections, any cookie you see may have been set using an insecure connection.

同时,感兴趣的同学也可以浏览下紫云飞大大的这篇博文: SameSite Cookie,防止 CSRF 攻击
rfc2109 与 rfc6265
对,新旧版本,自然而然有时候服务端的一些Set-Cookie会做向前兼容的处理

Dot prefix in cookie domain
domain中前置的点(dot)意味着这个cookie对于子域也同样有效(The leading dot means that the cookie is valid for subdomains as well)。但这是rfc2109种的规则,在rfc6265中,我们看到了这句话:

The Domain attribute specifies those hosts to which the cookie will be sent. For example, if the value of the Domain attribute is "example.com", the user agent will include the cookie in the Cookie header when making HTTP requests to example.com, www.example.com, and www.corp.example.com. (Note that a leading %x2E ("."), if present, is ignored even though that character is not permitted, but a trailing %x2E ("."), if present, will cause the user agent to ignore the attribute.) If the server omits the Domain attribute, the user agent will return the cookie only to the origin server.

rfc6265 会直接忽略前置的点,其实算是对老版本的一种兼容了。所以如果你在浏览器的Cookie面板中看到domain列的值有前置点,不要惊讶~

说在最后
其实在写这篇cookie安全相关的博文的时候,我脑袋了又冒出了同源策略这个词,加上我们这里的Cookie的安全策略,其实所谈到的都是关乎于源的共享的一个问题,那么其实cookie的话,还加上了一个源之间的干扰隔离(Set-Cookie domain字段限制)。谁决定源(resource)的访问权,当然是源的拥有者(服务端),这就不难理解,你想去hack同源策略,或者说CORS,自然也是需要服务端"答应"的,但这个策略实际由浏览器实施,作用于想去跨源访问资源的文档或脚本。
相关链接:https://lancelou.com/post/browser-cookie-deeping

https://www.jianshu.com/p/643427c17877

ajax跨域请求中的cookie问题
update 另一个问题
ajax在进行复杂请求如PUT,POST,DELETE等时,当请求为cross domain request是,会先发一个OPTIONS请求确认服务器的跨域支持情况,在发送原来的请求,所以对于服务器,需要对OPTIONS请求做一次xiang'yin

遇到的问题
对于前后端分离的应用,使用ajax跨域请求时,默认情况下是无法传输cookie的。具体的异常表现如下

客户端发送给服务器的请求中不包含cookie信息
服务器返回给客户端的响应中包含了Set Cookie 的信息,但是在浏览器的cookie中,没有记录词条cookie信息

解决方法
需要前后端都做一些小的改动

服务器端
以nodejs的后端为例,使用express框架,需要加上几行代码

app.all('*', function(req, res, next) {  
  res.header("Access-Control-Allow-Origin", config().allow_origin);  
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");  
  res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");  
  res.header("Access-Control-Allow-Credentials", "true");  
  res.header("X-Powered-By", ' 3.2.1')  
  res.header("Content-Type", "application/json;charset=utf-8");  
  next();  
});

注意这句话:
res.header("Access-Control-Allow-Credentials", "true");

这句话用来允许跨域访问时带上cookie信息,此外有一个问题,就是当我们"Access-Control-Allow-Origin"设置为的时候,上面这句话是无法使用的。所以不能够设置为,否则无法使用cookie。

浏览器端
以jquery的ajax请求为例

$.ajax({  
        url,  
        type: 'get',  
        dataType: 'json',  
        // 允许跨域  
        crossDomain: true,  
        // 下面这句话允许跨域的cookie访问  
        xhrFields: {  
          withCredentials: true  
        },  
        success: (res) => {  
          console.log(res);  
        }  
      });

总结
这个问题就是这样解决的,建议只允许自己前端网站的域名进行跨域访问,防止CSRF之类的攻击。

作者:cooody
链接:https://www.jianshu.com/p/643427c17877
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 关注 分享

要回复文章请先登录注册