同源策略
同源策略是用来限制从一个源加载的文档或脚本如何与其他源的资源进行交互的很重要的安全机制,这有助于隔离存在潜在威胁的文档,并减少可能的攻击向量。
关于「源」
网络内容的「源」是由在访问它时所用的 URL 的协议、主机(域名)和端口来定义的,只有当这三者在正在交互的两个对象中完全相同时才会被认为是「同源」。
下表给出了与 https://ourai.ws/categories/
进行源比较的例子:
URL | 结果 | 原因 |
---|---|---|
https://ourai.ws/posts/ |
◎ | 仅路径不同 |
https://ourai.ws/works/ |
◎ | 仅路径不同 |
http://ourai.ws/notes/ |
× | 协议不同 |
https://ourai.ws:8888/about/ |
× | 端口不同,https:// 默认是 443 端口 |
https://otaku.ourai.ws/categories/ |
× | 主机(域名)不同 |
源的继承
在页面中通过含有 about:blank
或 javascript:
的 URL 执行的脚本会继承所在文档的源,因为这些类型的 URL 并未包含源服务器的信息。
例如,about:blank
通常作为父脚本写入内容的新的空白弹出窗口的 URL(例如,通过 window.open()
机制)。如果此弹出窗口也包含代码,则该代码将继承与创建它的脚本相同的源。
data:
URL 会获得新的、空的安全上下文。
IE 中的例外
在同源策略上 IE 有两个主要的不同点:
- 两个相互之间高度互信的域名(trust zones),如公司内网域名(corporate intranet domains),不遵守同源策略的限制;
- 未将端口号加入到同源策略的组成之中,因此
https://ourai.ws:8888/
和https://ourai.ws/
属于同源并且不受任何限制。
跨源访问
因为浏览器的同源策略,在有访问不同来源的资源的需求时会造成一定的阻碍,这时需要根据具体情况采取一些手段才能达到目的。
更改源
在页面中能够有限制地更改它的源,可以通过脚本设置 document.domain
的值为页面的当前域或其当前域的父域。如果将其设置为当前域的父域,则这个更短的父域将用于之后的同源检测。
假如在 https://otaku.ourai.ws/categories/
中的一个脚本执行了以下语句:
document.domain = 'ourai.ws';
之后,这个页面会通过与 https://ourai.ws/categories/
的同源检测(假设 https://ourai.ws/categories/
已将 document.domain
设置为 ourai.ws
以表明它希望允许这样做)。然而,不能将 document.domain
设置为 oreilia.com
,因为它不是当前域的父域。
端口号是由浏览器另行检查的。任何对 document.domain
的赋值操作,包括 document.domain = document.domain
,都会导致端口号被重写为 null
。因此 ourai.ws:8888
不能仅通过设置 document.domain = 'ourai.ws'
来与 ourai.ws
通信。必须在他们双方中都进行赋值,以确保端口号都为 null
。
当使用 document.domain
允许一个子域去安全地访问它的父域时,需要将被访问页面中的 document.domain
设置为同一个值。即使这么做只是将父域设置回其原始值,但这是必要的,否则会导致权限错误。
跨源网络访问
同源策略控制着不同源之间的交互,比如在使用 XMLHttpRequest
或 <img>
元素时。这些交互通常分为三类:
- 跨源写操作,像链接、重定向、表单提交等通常是允许的,一些 HTTP 请求需要预检;
- 跨源嵌入资源通常是允许的(下面有例子);
- 跨源读操作通常是不允许的,但可以通过嵌入资源来巧妙地进行读取访问,如读取内嵌图片的尺寸、内嵌脚本的操作或其他妙不可言的事情。
下面是一些可以被嵌入跨源的资源的例子:
<script src="..."></script>
引入的 JavaScript,只有在同源脚本中才能看到语法错误的详情;<link rel="stylesheet" href="...">
引入的 CSS,由于 CSS 松散的语法规则,跨源 CSS 需要设置正确的Content-Type
头信息(限制因浏览器而异:IE、Firefox、Chrome、Safari 和 Opera);<img>
显示的图片;<video>
和<audio>
播放的媒体;<object>
、<embed>
和<applet>
嵌入的插件;@font-face
引入的字体,一些浏览器允许跨源字体,一些需要同源;<frame>
和<iframe>
嵌入的任何资源,网站可以通过添加X-Frame-Options
头信息阻止被跨源嵌入。
允许跨源访问
主流的跨源访问方式是「跨源资源共享」,英文为「cross-origin resource sharing」,简称「CORS」,它使服务器指定哪些主机可以加载资源内容,主要用于 AJAX。
此方式依赖于服务器在头信息中设置 Access-Control-Allow-Origin
。如果是允许所有来源的请求访问,使用通配符 *
;若只允许某个来源,则要指明来源域名:
Access-Control-Allow-Origin: https://api.ourai.ws
在很多情况下,发送请求时需要附带 Cookie 等信息给服务器,这时就必须得指定来源域名了,并且还要在头信息中设置 Access-Control-Allow-Credentials
:
Access-Control-Allow-Origin: https://api.ourai.ws
Access-Control-Allow-Credentials: true
另外,发送请求的 JavaScript 代码也需要做出相应的改变:
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.ourai.ws/posts', true);
xhr.withCredentials = true; // 这个一定要设置
xhr.onreadystatechange = function() {
// ...
}
xhr.send();
若是使用 jQuery 发送请求:
$.ajax({
url: 'https://api.ourai.ws/posts',
type: 'GET',
xhrFields: {
withCredentials: true
},
success: function() {
// ...
}
});
遗憾的是,有些浏览器不支持 CORS,可以使用「JSONP」作为替代方案。
拦截跨源访问
- 要阻止跨源写操作,可以在请求时检测一个难以预测的 token,即 CSRF token,使用这个 token 可以阻止跨源读操作;
- 要阻止跨源读操作,需要确保资源是不能被嵌入的,阻止嵌入是必要的,因为嵌入资源会泄漏一些相关信息;
- 要阻止跨源嵌入,需确保被访问资源不能解析成上述可嵌入格式。