浏览器同源策略研究
什么是同源策略?
同源策略限制了一个源(origin)上加载的资源或脚本与其他源上的资源进行交互的行为.
这样的跨域交互主要分为三类:
-
cross-origin writes: 包括(均为指向其他源的)超链接, 重定向以及表单请求(见下文说明)等. 这一类请求基本允许.
-
cross-origin embedding: 包括
<script>
, css link,<img>
,<video>
,<audio>
,<frame>/<iframe>
等. 这一类请求基本允许. -
corss-origin reads: 包括使用ajax获取其他源的数据, 以及使用js读取其他iframe上的DOM信息等. 这一类请求基本不允许.
这里需要着重说明的是所谓不允许是什么意思. 这的不允许是指, 跨域请求无论如何都可以正常发送, 但是你将拿不到这个请求的返回值. 这样一来, 在跨域的前提下普通的表单请求依然可以正常工作(因为本来也拿不到返回值, 也不需要返回值). 而对于Ajax请求来说, 请求虽然可以正常发送到服务器, 但是浏览器会阻止js代码将拿到服务器的返回值.
看起来同源策略很严密了? 其实不然. 现行的同源策略有几个弱点, 举例如下:
-
CSRF攻击, 即恶意站点伪造指向其他被害网站的表单并提交, 从而修改用户在被害网站上的数据
-
XSS攻击. 攻击者在被害网站中植入js脚本(比如网站没有做js或html标签过滤), 获取被害网站中的敏感数据(如cookies)后通过http请求发送到自己的服务器上(比如使用
<a>
,<img>
或者构造表单发送). 只要能发送出去服务器上就会有记录, 从而窃取机密数据. -
跨域信息泄露. 简单的技术比如跨域加载的img虽然不能读取内容, 但是可以获取其宽高的数据. 复杂的技术甚至可以探知用户有没有登录某些网站. (这种攻击方式参见https://www.grepular.com/Abusing_HTTP_Status_Codes_to_Expose_Private_Information)
CORS
在默认情况下, 同源策略将按照上述的三个规则进行工作. 但是有时候我们想对同源策略进行调整, 人为地定制(主要是放宽)一些同源策略限制来满足我们的开发需求(俗称"绕过"同源策略). 其中最常用的方法就是使用CORS标准.
前面说到, 即使违反了同源策略, 跨域请求依然是可以发出去的, 只是收不到响应而已(对于Ajax请求来说就是, 响应被浏览器丢弃, 不交给对应的js回调函数). 收不到响应的原因根本上说是存在同源策略, 而直接原因就是浏览器执行了CORS检查.
如上图所示, 服务器在返回响应时, 会加上一个Access-Control-Allow-Origin的Header, 用来告诉用户浏览器这个响应允许被哪些domain上的js读取. 上图的例子中, *
表示任意domain都可以读取. 当浏览器发现当前domian不在这个列表中的话, 就会执行响应拒绝. 所以, 如果要允许跨域请求, 只需要调整该字段的值即可. 注意, 如果不指定这个字段, 那么将会执行默认的同源策略限制.
另外还注意到, 发出HTTP请求时还会带着Origin的Header, 这个Header就是发出请求时的domain. 这个字段的一个用法是可以用来对抗CSRF. 比如可以在服务器端对用户请求进行检查, 如果发现Origin不符合就直接拒绝.
上述的流程只限于简单的HTTP请求, 即同时满足:
-
GET, HEAD, POST 三个方法之一
-
只允许修改Accept, Accept-Language, Content-Language以及Content-Type三个报文头(其他必要报文头由浏览器自行填充, 但用户端js不能修改)
-
Content-Type为
application/x-www-form-urlencoded
,multipart/form-data
,multipart/form-data
三者之一
如果有任意一点不满足, 则会先发送一个preflight请求向服务器询问是否允许发送, 得到允许后再发出实际请求.
这里值得重点强调的是preflight存在的意义. preflight本身其实和同源策略或者CORS一点关系也没有, 其主要功能就只是确认服务器是否支持某些请求或操作. 从而避免出现服务器不支持的情况 (比如上古服务器软件或者没有开启某些HTTP操作的服务器), 因此属于兼容性测试的一种. 如果发现服务器不支持, 那么就可以提前终止后续请求的发送.
其他绕过同源策略的技术
具体的技术细节参见: http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html
当两个网页的一级域名相同时, 可以通过修改codument.domain
来实现cookies的共享.
而对于两个毫无关系的网页来说, 有以下方式可以实现同源策略的规避:
1. fragment identifier
父页面可以修改子页面(iframe)url中#后的部分而不会引起子页面刷新. 这样就能向子页面传递数据
2. window.name
父页面打开子窗口(window)的情况下, 子窗口的window.name字段不会随子窗口页面的跳转而变化, 因此可以用于在父子窗口中进行信息交换. 还可以对其进行监听.
3. window.postMessage
不同于前两个hack出来的技术. window.postMessage
是为了解决跨域通信问题而设计的HTML5 API.
详细信息可参见: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
4. JSONP
JSONP可以从非同源的网站上获取json数据.
一般而言, 要获取一个json数据最常用的方法就是构造一个Ajax请求. 但是这样会被同源策略拦截.
另一个略显奇怪的方法是, 使用<script>
标签加载一个在另一个源上的js文件(这属于cross-origin embedding, 是允许的), 而在这个文件中声明了一个全局变量, 值就是你想返回的json数据. 加载后请求端的js就可以读取这个全局变量来获得想要的json数据. 这是完全可行的也实现了跨域数据获取的问题, 但是这样不太方便, 于是就有了JSONP技术.
简单来说就是使用类似于
<scirpt src="http://example.com/ip?callback=foo\"></script>
的写法. 当加载完成后浏览器会自动调用foo函数并传入解析后的json作为第一个参数.
这种方法的局限在于只支持GET操作并只能返回JSON数据.