同源策略应该是学习Web安全时最最基础的内容,时隔多年再回过头来仔细温习一下,发现了不少当初漏掉的细节,结合这么多年的安全经验,重新总结一下同源策略相关的内容。
如果两个页面拥有相同的协议(http、https)、相同的端口(如果其中一个指定了端口)、相同的host,那么就可以认为这两个页面是同源的。简单的讲,同源策略就是同协议、同端口、同host这样的一个三元组,这里给出几个例子:
那么这种限制有什么作用呢?同源策略主要是限制了页面最后那个的脚本从另一个源加载资源时的行为,这对于防范恶意页面是一种很好的防御机制,如果恶意脚本请求了非同源的一个东西,那么这种行为就很可能因为同源策略的限制被浏览器拒绝,从而在某种程度上缓解了攻击。
对于about:blank
和javascript:
这种特殊的URL,他们的源应当是继承自加载他们的页面的源,他们本身并没有『源』的概念。
我们已经知道了,浏览器会根据同源策略允许或拒绝加载某些资源,但是又一个问题由此而生,我们的网站通常会将静态文件(CSS
,JS
,图片)等放置在CDN上,那么CDN与当前域必然是不同源的,但是神奇的是,这些网站可以正常加载出他们需要的资源并展示给用户,这里为什么又不受同源策略的影响呢?
再比如在使用XMLHttpRequest
的时候,又会因为同源策略的限制无法发出请求,那么到底什么情况下会触发同源策略呢?总体来说,页面跨域的行为主要会分为三类,分别是:
在这三种行为之中,通常情况下只有Cross-origin read
是不被允许的,其余的两种是允许的,例如Cross-origin write
中的links
,重定向以及表单提交,Cross-origin embedding
中的资源嵌入。
那么问题又来了,何种资源是允许嵌入的呢?MDN文档中也给出了一些例子:
<link rel="stylesheet" href="...">
标签嵌入CSS。由于CSS的松散的语法规则,CSS的跨域需要一个设置正确的Content-Type
消息头,不同浏览器有不同的限制。
@font-face
引入的字体。一些浏览器允许跨域字体(cross-origin fonts),一些需要同源字体(same-origin fonts)。
<frame>
和<iframe>
载入的任何资源。站点可以使用X-Frame-Options消息头来阻止这种形式的跨域交互。
这样一来,可以通过<img>
标签加载文件而不受同源策略的影响这件事情就明白了。
刚刚我们已经知道了,一些标签嵌入外域数据的时候,是不会受到同源策略的影响,但是如果我们在<script></script>
脚本中想要获取外域的数据时,因为同源策略的干扰,就显的格外麻烦。出于安全考虑,浏览器通常情况下都会限制从script
标签内部发起的跨域HTTP请求,比如前文提到的XMLHttpRequest
,就会受到同源策略的影响,进而只能将数据发到同域内。
于是就有了CORS策略,允许Web应用程序进行跨域访问。CORS的全称是cross-origin sharing stander
,跨域资源共享标准。简单的讲,这个标准允许在以下的几个场景中发起跨域请求:
在这些允许跨域的场景之中,我们作为安全人员,最关心的自然是scripts
部分。
CORS
通过一些特殊的HTTP头来确保哪些源站可以请求哪些资源,除此之外,如果这个请求会对服务器的数据产生修改的可能
(这个说法并不是很准确,后面会详细阐述),将会在跨域之前发起一个preflight
请求进行检查,如果检查不通过,那么不会发起跨域请求。
首先要明确一点,并不是所有的请求都会触发preflight
机制,这些不会触发preflight
的请求被称为simple request
。
符合下列条件的请求,将不会触发preflight
机制并视为simple request
:
凡是不满足上述条件的请求,将被视为preflight request
,并在发起跨域请求前,预先发起一个OPTIONS
请求进行检查。在preflight request
的返回头中,会包含一些关于是否允许发起跨域的信息。例如我们看这个preflight
的例子,这个是向一个地址POST XML内容的请求:
OPTIONS /resources/post-here/ HTTP/1.1 Host: bar.other User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre 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: http://foo.example Access-Control-Request-Method: POST Access-Control-Request-Headers: X-TEST, Content-Type HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:39 GMT Server: Apache/2.0.61 (Unix) Access-Control-Allow-Origin: http://foo.example Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Headers: X-TEST, Content-Type Access-Control-Max-Age: 86400 Vary: Accept-Encoding, Origin Content-Encoding: gzip Content-Length: 0 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Content-Type: text/plain
在OPTIONS
请求中,发出了两个特殊的HTTP头,分别是Access-Control-Request-Method
和Access-Control-Request-Headers
,意思是告诉服务端,我接下来的请求中,会使用POST
方法,并且会携带两个自定义的头部字段X-TEST
和Content-Type
(因为Content-Type
的内容并非是我们上文中提到的三种之一,所以被看做是自定义头部)。
Access-Control-Allow-Origin: http://foo.example Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Headers: X-TEST, Content-Type Access-Control-Max-Age: 86400
从第一行开始,表示允许来自(http,foo.example,80)
这个源发来的数据,允许的方法为POST,GET,OPTIONS
,允许使用自定义的头部X-TEST
和Content-Type
,该响应的有效时间是86400
秒,如果在这个期间内,客户端无需为了同样的跨域请求再次发起preflight request
,通常情况下,每个浏览器都有自己的最大时间以避免出现某些安全问题。
最后呢,神奇的是CORS还允许通过设置Cookies或HTTP认证来发送认证信息。如果发起的是一个简单请求,那么不会经过preflight
,但是有一点需要注意,如果服务端返回的信息中没有Access-Control-Allow-Credentials: true
,浏览器就会拦截返回内容,不会将内容返还给调用者。