浅谈同源策略

Same-origin Policy

同源策略可能是现代浏览器中最重要的安全概念了,它在使得同一站点中各部分页面之间基本上能够无限制允许脚本和其他交互的同时,能完全防止不相关的网站之间的任何干涉。现在所有支持 JavaScript 的浏览器都会使用这个策略,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。

我们可以假设一个没有同源策略的场景:我打开了我自己的银行账户页面,称之为 A,之后,我又打开了另一个页面,我们称之为 B。如果 B 是一个恶意页面,那么在没有同源策略限制的前提下,它可以通过 Javascript 任意修改和访问 A 中的任何内容。

一、什么叫做同源

首先要厘清的是,怎么样的页面被称为同源的页面——如果两个页面的协议、端口以及域名都相同,那这两个页面就被称之为同源,如果其中有一项不同,那也将不会满足同源的定义。下面是以 http://store.company.com/dir/page.html 这个 URL 为源的一组例子:

URL 是否同源 原因
http://store.company.com/dir2/other.html
http://store.company.com/dir/inner/another.html
https://store.company.com/secure.html 不同的协议
http://store.company.com:81/dir/etc.html 不同的端口
http://news.company.com/dir/other.html 不同的域名

将表格中所有的 URL 与 http://store.company.com/dir/page.html 相比较,我们可以看到:

  • 第一条和第二条同时满足了协议、端口和域名相同的条件,所以是同源;
  • 第三条因为使用的是 https 协议,协议不同,所以不是同源;
  • 第四条因为使用了 81 端口,端口不同,所以不是同源;
  • 第五条因为域名,也就是域名不相同,所以不是同源。

那么为什么会对于同源做出如此严格的限制呢,其实是否同源主要是为了防止两类事件:

  • 限制跨源脚本的 APIs 的访问;
  • 阻止跨源数据存储的访问。

简单来说就是防止一个恶意界面通过恶意请求去访问非同源的数据。在发起跨域请求的情况下,我们的浏览器会自动的去拒绝这些请求,即使这样的跨域请求通过了,其返回结果也会被浏览器拒绝。

二、跨源网络访问

同源策略会对于跨域的资源和数据的访问做出限制。其实在网上很多情况下我们都会需要加载不同源的资源,比如在个人网站中需要插入一张在公共图床的图片,这种情况下个人网站和公共图床上的图片必然是不同源的,但最后在页面上能成功的加载图片并且能够看到,这又是为什么呢?

首先,通常情况下同源策略控制跨域的请求会被分为三类:

  • 跨域写操作Cross-origin writes )-- 例如表单提交,通常是被允许的;
  • 跨域读操作Cross-origin reads )-- 例如可以读取嵌入图片的高度和宽度,通常是不被允许的;
  • 跨域资源嵌入Cross-origin embedding )-- 例如嵌入图片,通常是被允许的。

那么可能有哪些资源是可以被跨源嵌入的呢?

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

如果说文件类型符合以上几种,那么其实这样的资源是可以被跨域嵌入的。现代浏览器在安全性和可用性之间选择了一个平衡点,在遵循同源策略的基础上,选择性地为同源策略“开放了后门。这也解释了为什么放在公共图床上的图片能够被正确的浏览的问题。

三、跨域资源共享(CORS)

因为同源策略的限制,如果在脚本内发起了跨域的 HTTP 请求,是不会得到返回结果的,最常用的应该就是 XMLHttpRequest 。如果想要获取跨域的资源,同源策略就会成为一种枷锁,使得数据的正常交互十分麻烦。而 CORS 则解决了这个问题。

CORS 的全称为 Cross-Origin Resource Sharing跨域资源共享。这是一个由一系列传输的 HTTP 头组成的系统,这些 HTTP 头用于确定阻止还是接受从该资源所在域外的另一个域的网页上发起的对受限资源的请求。CORS 允许 Web 应用服务器进行跨域访问控制,从而使跨域数据传输得以安全进行。

简单的来说,CORS 允许在以下几种场景中使用跨域 HTTP 请求:

  • 由 XMLHttpRequest 或 Fetch 发起的跨域 HTTP 请求;
  • Web 字体( CSS 中通过 @font-face 使用跨域字体资源)。因此,网站就可以发布 TrueType 字体资源,并只允许已授权网站进行跨站调用;
  • WebGL 贴图;
  • 使用 drawImage 将 Images/video 画面绘制到 canvas;
  • 样式表(使用 CSSOM)。

四、预检请求(Preflight Request)

前面已经解释了 CORS 会在请求 HTTP 请求中加入一些特殊的 HTTP 头来规定特定的资源能被跨域请求,除了这些特殊的 HTTP 头之外,CORS 利用预检请求的方式在跨域之前对一些特定的请求进行检查,如果检查响应的结果没有通过,那么跨域请求也不会发起。

预检请求会发生在以下几种情况中:

  • GETPOST 方法的请求;
  • POST 请求中 Content-Type 字段不是 application/x-www-form-urlencodedmultipart/form-datatext/plain
  • 发送自定义的头信息,比如 X-PINGARUNER

除了这些请求,还有一些请求被称为简单请求,简单请求不会触发 CORS 的预检请求:

  • 请求方法为下列方法之一:
    • GET
    • HEAD
    • POST
  • HTTP 首部字段仅限下面这个集合:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink
    • Save-Date
    • Viewport-Width
    • Width
  • Content-Type 的值仅限于下列三中:
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded

下面我们通过一个例子来了解整个预检请求的过程(例子中的 HTTP 头信息都经过省略,只保留关键的几条字段):

如果需要向服务器发送下面这个 POST 请求,该请求会发送一个 XML 文档,同时包含了一个自定义的请求首部字段。

POST /doc HTTP/1.1
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Origin: Server-b.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type

那么首先浏览器会判断该请求是否是简单请求。因为该请求的 Content-Typeapplication/xml,也包含自定义的请求首部字段,所以在真正发送该 POST 请求之前,会先发起一个预检请求。

下面这个 OPTIONS 请求其实就是预检请求,该请求利用 Access-Control-Request-Method 告诉服务器,接下来的实际请求的方法是 POST,再利用 Access-Control-Request-Headers 告诉服务器,这个实际请求还会包含两个自定义请求的首部字段。

OPTIONS /resources/post-here/ HTTP/1.1
Origin: Server-b.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type

和普通的 HTTP 请求一样,预检请求也会返回一个响应。在下面这个响应中,Access-Control-Allow-Origin允许了来自 http://foo.example 这个源发来的数据。Access-Control-Allow-Methods 表示允许的方法为 POSTGETOPTIONSAccess-Control-Allow-Headers 表示允许了自定义的首部字段。最后 Access-Control-Max-Age 表明该响应的有效时间为 86400 秒,在有效时间内,浏览器就不需要为同一请求再次发起预检请求,如果该首部字段的值超过了最大有效时间,将不会生效。每个浏览器都会有自己的最大有效时间,设置该限制的目的是为了避免一些安全问题。

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400

最后之前的 HTTP 请求才会发送,得到最终的响应。

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://foo.example

除了简单请求和预检请求之外,CORS 还允许设置 HTTP cookies 和 HTTP 认证信息发送身份凭证。(黄缪华 | 天存信息)

Ref

(完)