同源策略应该是学习Web安全时最最基础的内容,时隔多年再回过头来仔细温习一下,发现了不少当初漏掉的细节,结合这么多年的安全经验,重新总结一下同源策略相关的内容。

0x00 何为同源策略

如果两个页面拥有相同的协议(http、https)、相同的端口(如果其中一个指定了端口)、相同的host,那么就可以认为这两个页面是同源的。简单的讲,同源策略就是同协议、同端口、同host这样的一个三元组,这里给出几个例子:

SOP

我们将所有的URL都与第一个URL相比:

  • 第二个与第一个URL是相同的,原因是满足我们上面提到的三元组(协议、端口、host)
  • 第三个与第一个相比,其协议不同;
  • 第四个与第一个相比,其端口不同;
  • 第五个与第一个相比,其host不同;

那么这种限制有什么作用呢?同源策略主要是限制了页面最后那个的脚本从另一个源加载资源时的行为,这对于防范恶意页面是一种很好的防御机制,如果恶意脚本请求了非同源的一个东西,那么这种行为就很可能因为同源策略的限制被浏览器拒绝,从而在某种程度上缓解了攻击。

对于about:blankjavascript:这种特殊的URL,他们的源应当是继承自加载他们的页面的源,他们本身并没有『源』的概念。

0x01 HTTP请求控制

我们已经知道了,浏览器会根据同源策略允许或拒绝加载某些资源,但是又一个问题由此而生,我们的网站通常会将静态文件(CSS,JS,图片)等放置在CDN上,那么CDN与当前域必然是不同源的,但是神奇的是,这些网站可以正常加载出他们需要的资源并展示给用户,这里为什么又不受同源策略的影响呢?

再比如在使用XMLHttpRequest的时候,又会因为同源策略的限制无法发出请求,那么到底什么情况下会触发同源策略呢?总体来说,页面跨域的行为主要会分为三类,分别是:

  • Cross-origin write
  • Cross-origin read
  • Cross-origin embedding

在这三种行为之中,通常情况下只有Cross-origin read是不被允许的,其余的两种是允许的,例如Cross-origin write中的links,重定向以及表单提交,Cross-origin embedding中的资源嵌入。

那么问题又来了,何种资源是允许嵌入的呢?MDN文档中也给出了一些例子:

  • <script src="..."></script>标签嵌入跨域脚本。语法错误信息只能在同源脚本中捕捉到。
  • <link rel="stylesheet" href="...">标签嵌入CSS。由于CSS的松散的语法规则,CSS的跨域需要一个设置正确的Content-Type消息头,不同浏览器有不同的限制。
  • <img>嵌入图片。支持的图片格式包括PNG,JPEG,GIF,BMP,SVG,...
  • <video><audio>嵌入多媒体资源。
  • <object>, <embed><applet>的插件。
  • @font-face引入的字体。一些浏览器允许跨域字体(cross-origin fonts),一些需要同源字体(same-origin fonts)。
  • <frame><iframe>载入的任何资源。站点可以使用X-Frame-Options消息头来阻止这种形式的跨域交互。

这样一来,可以通过<img>标签加载文件而不受同源策略的影响这件事情就明白了。

0x02 CORS又是什么东西

刚刚我们已经知道了,一些标签嵌入外域数据的时候,是不会受到同源策略的影响,但是如果我们在<script></script>脚本中想要获取外域的数据时,因为同源策略的干扰,就显的格外麻烦。出于安全考虑,浏览器通常情况下都会限制从script标签内部发起的跨域HTTP请求,比如前文提到的XMLHttpRequest,就会受到同源策略的影响,进而只能将数据发到同域内。

这里需要说明一点,这里的跨域请求可能不是浏览器直接拦截掉了,而是跨站请求发起了,但是返回结果被浏览器拦截了,请求实际上已经发送到了后端服务器。在ChromeFirefox上,对于从httpshttp的跨域是会直接拦截,请求都无法发送成功。

于是就有了CORS策略,允许Web应用程序进行跨域访问。CORS的全称是cross-origin sharing stander,跨域资源共享标准。简单的讲,这个标准允许在以下的几个场景中发起跨域请求:

  • XMLHttpRequestFetch等发起的跨域请求;
  • Web字体,通过@font-face进行跨域调用;
  • WebGL贴图;
  • 使用 drawImage 将 Images/video 画面绘制到 canvas;
  • 样式表(使用 CSSOM);
  • scripts;

在这些允许跨域的场景之中,我们作为安全人员,最关心的自然是scripts部分。

0x03 进击的CORS

CORS通过一些特殊的HTTP头来确保哪些源站可以请求哪些资源,除此之外,如果这个请求会对服务器的数据产生修改的可能(这个说法并不是很准确,后面会详细阐述),将会在跨域之前发起一个preflight请求进行检查,如果检查不通过,那么不会发起跨域请求。

首先要明确一点,并不是所有的请求都会触发preflight机制,这些不会触发preflight的请求被称为simple request

符合下列条件的请求,将不会触发preflight机制并视为simple request

  • GET请求
  • HEAD请求
  • Content-Type为指定值的POST请求,包括text/plainmultipart/form-data以及application/x-www-form-urlencode
  • HTTP首部字段不能包含下列以外的值:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width

凡是不满足上述条件的请求,将被视为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-MethodAccess-Control-Request-Headers,意思是告诉服务端,我接下来的请求中,会使用POST方法,并且会携带两个自定义的头部字段X-TESTContent-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-TESTContent-Type,该响应的有效时间是86400秒,如果在这个期间内,客户端无需为了同样的跨域请求再次发起preflight request,通常情况下,每个浏览器都有自己的最大时间以避免出现某些安全问题。

最后呢,神奇的是CORS还允许通过设置Cookies或HTTP认证来发送认证信息。如果发起的是一个简单请求,那么不会经过preflight,但是有一点需要注意,如果服务端返回的信息中没有Access-Control-Allow-Credentials: true,浏览器就会拦截返回内容,不会将内容返还给调用者。

0xFF 参考文献