毁人不倦-令人困惑的浏览器安全策略:同源策略

同源策略(The same-origin policy)

这是浏览器的一个基本却又非常重要的安全策略,浏览器会限制对异源(异域)(我们常称之为别人家的站点)的资源操作。打个比方,你不会让老王来你家,也不允许他在你家墙上打个洞,装个监控啥的。通过这个比喻你就知道同源策略的重要性了。

同源策略主要针对脚本(script)的行为进行限制,而<script>,<link>,<img>,<iframe>,<object>等带有src属性的dom元素一般不受影响,这也很好理解,你可以禁止老王进你家,但是无法限制他在自己家装个雷达对你家进行监控。因此,在没有得到授权的情况下,用javascript脚本操作异域的资源,那是不允许的。科学的说法应该是:浏览器允许发起请求,但如果响应中没有包含对方的许可的话,浏览器就会屏蔽响应结果,不给你用。经常会抛出这样的异常: XMLHttpRequest cannot load http://www.othersite.com/. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://www.mysite.com' is therefore not allowed access.

源(Origin)

唠叨了半天异源(异域),科学的解释一下什么是源(Origin)

公式:Origin = [protocol]://[domain]:[port]

Origin是你的页面所在的位置。例如我有个站点www.mysite.com,那么它的就是http://www.mysite.com:80,其中[protocol]的缺省值是http[port]的缺省值是80

不管我的站点在哪个页面www.mysite.com/p/1.html,只要[protocol],[domain],[port]三者相同则视为同源,或者叫同域,通常称之为同域,因为我们通常都是叫别人的小名二狗子,而不会称呼其大名犬次郎

更多示例:

URL Origin
http://www.mysite.com/p/1.html http://www.mysite.com
https://wwww.mysite.com/p/1.html https://www.mysite.com
http://app.mysite.com/p/1.thml http://app.mysite.com
http://www.mysite.net/p/1.html http://www.mysite.net
http://www.mysite.com:9000/p/1.html http:www.mysite.com:9000
http://www.mysite.com/news/fresh.html http://www.mysite.com

解决访问跨域资源的问题

虽然是安全了,但是如果想从我的www.mysite.com去我的分站son.mysite.com获取点东西也会被同源策略禁止,这就不是我们想要的了,那怎么办呢?可以利用<script>等不受同源策略限制的dom元素绕过去,这种方式称之为jsonp,这是很多年前就提出来的方法,很巧妙不过很繁琐,渐渐地不怎么再使用了,有兴趣的自行google

还有现代化的解决方案:CORS(Cross-Origin Resource Sharing)。还记得同源策略的规定吗?通过屏蔽响应结果的方式保证信息安全,也就是说浏览器并没有阻止发起跨域请求。浏览器如果在跨域请求的响应(Http Response Headers)中发现了对方的许可就会认为是安全的。这套标准称之为CORS。

这套标准规定一系列的Http Headers,让服务器申明哪些资源是可以被谁访问,浏览器通过解析响应头部就能知道是否得到了许可。

举两个简单的🌰栗子说明这一系列的Http Headers:

  1. www.mysite.com中有如下脚本,要去访问www.othersite.com的资源。
<script>
var request = new XMLHttpRequest();
var url = 'www.othersite.com/post/001/';
   
function callOtherDomain() {
  if(request) {    
    request.open('GET', url, true);
    request.onreadystatechange = handler;
    request.send(); 
  }
}
</script>

通过浏览器的控制台,查看到该请求,请求和响应报文的重要内容如下:

请求报文

GET /post/001 HTTP/1.1
Host: www.othersite.com
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Referer: www.mysite.com
Origin: http://www.mysite.com

响应报文

HTTP/1.1 200 OK
Date: Sat, 04 Mar 2017 14:23:53 GMT
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked

这是一个简单的Get请求,在请求报文中有一个值得注意的Origin,这个属性就表示了当前所处的,Origin是由浏览器自动控制的,不允许用户干预。如果同源策略判定请求是跨域请求,那么就会自动把Origin加入请求头部中。

在响应报文中,注意Access-Control-Allow-Origin属性,如果对方允许你跨域访问,那么它会在响应中加入你的请求头部中的Origin,此处的响应报文中的*表示允许任何请求的跨域访问。

  1. https://www.mysite.com要去修改https://posts.mysite.com中的一篇文章。
var request = new XMLHttpRequest();
var url = 'posts.mysite.com/?pid=1024';
var body = 'new post';

function callOtherDomain(){
  if(request)
    {
      request.open('PUT', url, true);
      request.setRequestHeader('userid', 'keke');
      request.onreadystatechange = handler;
      request.send(body); 
    }
}

这次是发送了PUT类型的请求,要去修改pid=1024的文章,同时还携带了我的身份信息userid=keke在请求头部中。继续在浏览器的控制台中观察请求信息,发现有两次请求,第一个如下:

OPTIONS /?pid=1024
Host: https://posts.mysite.com
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: https://www.mysite.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: userid


HTTP/1.1 200 OK
Date: Sat, 04 Mar 2017 14:35:39 GMT
Access-Control-Allow-Origin: https://www.mysite.com
Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT
Access-Control-Allow-Headers: userid
Access-Control-Max-Age: 1728000
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

这是因为这次发送的不是简单请求,CORS规范要求先发个预检请求(Preflight),一般会采用OPTIONS类型,该请求不包含请求体,会携带一些用于探测的信息,除了Origin,还有

* `Access-Control-Request-Method`
* `Access-Control-Request-Headers`

前者用来携带真正的请求的类型,后者携带真实请求自定义的头。

在响应报文中还有Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT表示对方许可的请求类型。

对方会根据预检请求中的信息判断是否可以接受真正的请求,如果预检请求通过了,浏览器才会发起真正的请求。

PUT /?pid=1024/ HTTP/1.1
Host: https://posts.mysite.com
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
userid: keke
Content-Type: text/xml; charset=UTF-8
Referer: https://www.mysite.com
Content-Length: 8
Origin: https://www.mysite.com
Pragma: no-cache
Cache-Control: no-cache
……



HTTP/1.1 200 OK
Date: Sat, 04 Mar 2017 14:35:39 GMT
Access-Control-Allow-Origin: https://www.mysite.com
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
……


响应报文报文头部中带有Access-Control-Allow-Origin: https://www.mysite.com,表示对方同意
https://www.mysite.com的请求,浏览器就不会屏蔽响应结果。

上述栗子中,提到了CORS规定了对于不简单的请求类型,要先发一个预检请求(Preflight),那么简单与否的判定条件是什么呢?在如下范围内的请求都是被视为简单请求

  1. 请求类型的范围限制:GET,HEAD,POST
  2. 自定义的请求头部限制范围:Accept,Accept-Language,Content-Language
  3. 媒体类型(Content-Type)的限制范围的:application/x-www-form-urlencoded,multipart/form-data,text/plain

除来上述栗子中提到的几个头部,还有哪些呢?如下明细:

HTTP请求头部

  1. Origin: 表示发送请求者的源(域),浏览器控制的。
  2. Access-Control-Request-Method: 这是预检请求(Preflight)中表示真实请求的请求方式。
  3. Access-Control-Request-Headers: 这是预检请求(Preflight)中表示真实请求的自定义的头部,可以有多个(Access-Control-Request-Headers: userid, pwd, location:表示真实请求会携带3个自定义头部(userid,pwd,location))。

HTTP响应头部

  1. Access-Control-Allow-Origin: <origin> | *: origin参数表示对方允许访问的URI.对于一个不带有credentials的请求,可以指定为'*',表示允许来自所有域的请求。
  2. Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header: 设置允许的请求头部。
  3. Access-Control-Max-Age: <delta-seconds>: 这个头告诉我们这次预检请求的结果的有效期是多久,delta-seconds 参数表示,允许这个预检请求的参数缓存的秒数,在此期间,不用发出另一条预检请求
  4. Access-Control-Allow-Credentials: true | false: 告知客户端,当请求的credientials属性是true的时候,响应是否可以被得到.当它作为预检请求的响应的一部分时,它用来告知实际的请求是否使用了credentials.注意,简单的GET请求不会预检,所以如果一个请求是为了得到一个带有credentials的资源,而响应里又没有Access-Control-Allow-Credentials头信息,那么说明这个响应被忽略了。
  5. Access-Control-Allow-Methods: <method>[, <method>]*: 这个响应头信息在客户端发出预检请求的时候会被返回,表示被允许的请求方式。
  6. Access-Control-Allow-Headers:<field-name>[, <field-name>]*: 也是在响应预检请求的时候使用。用来指明在实际的请求中,可以使用哪些自定义HTTP请求头。

对于Access-Control-Allow-Credentials这个头部只会出现在请求头部中包含了凭证(HttpCookie)信息。一般而言,对于跨站请求,浏览器是不会发送凭证信息的。但如果将XMLHttpRequest的一个特殊标志位设置为true,浏览器就将允许该请求的发送。

var invocation = new XMLHttpRequest();
var url = 'http://www.othersite.com';
    
function callOtherDomain(){
  if(invocation) {
    invocation.open('GET', url, true);
    invocation.withCredentials = true;
    invocation.onreadystatechange = handler;
    invocation.send(); 
  }

再例如使用jQuery发起ajax请求时:

$.ajax({
    //...
    xhrFields: {
        withCredentials: true
    },
    //...
});

针对脚本(script)的跨域操作是安全了,可是如果通过<iframe>这种dom元素来嵌入资源的话,同源策略就无法保护我们了。

针对<iframe>的安全策略

我们肯定不希望自己的页面被不法分子用<iframe src="www.mysite.com"></frame>等方式嵌入,然后被利用。浏览器们早已考虑到这个漏洞,并提出了解决方案。类似于CORS标准,解析响应头部中的X-Frame-Options,用这个头部信息来表达是否可以被对方嵌入。

X-Frame-Options有三种值:

X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN
X-Frame-Options: ALLOW-FROM https://www.othersite.com/
  1. DENY: 无论请求者是谁,都不允许嵌入。
  2. SAMEORIGIN: 只有同源(origin)的页面才可以嵌入。
  3. ALLO-From https://www.othersite.com/: 只有https://www.othersite.com/才可以嵌入咱们的页面。

例如,www.othersite.com要用如下代码嵌入我的页面www.mysite.com

...
<iframe src="www.mysite.com"></iframe>
...

我给www.mysite.com的响应头部中加入X-Frame-Options: SAMEORIGIN,浏览器会屏蔽响应结果,并报错Refused to display 'http://www.mysite.com/' in a frame because it set 'X-Frame-Options' to 'SAMEORIGIN'.

这些都是基本的网络安全规范,但是其重要性却不可忽略。面对红果果的互联网,时刻不能放松。

posted @ 2017-03-05 14:43  蝌蝌  阅读(2444)  评论(0编辑  收藏  举报