web 安全的基石就是同源政策。
何为同源
几个页面的协议、主机名、端口相同,那么就认为这些页面是同源的。
如与 http://book.nonuplebroken.com:80/index.php 比较:
对于 about:blank
和 javascript:
这种特殊的 URL,他们的源应当是继承自加载他们的页面的源。
何为同源策略
同源策略(Same Origin Policy,SOP)是 Web 应用程序的一种安全模型,主要是限制了页面从另一个源加载资源时的行为。而且既然只是一种模型,那么不同组织企业实现时就会有不少差异。
同源策略只作用在实现了同源策略的 WEB 客户端上。 现在网上对同源策略有一个错误的解释:只有和目标同源的脚本才会被执行。这是不对的,同源策略没有禁止脚本的执行,而是禁止读取 HTTP 回复。因此会发现,同源策略的作用其实很有限,如防止 CSRF 攻击。
限制范围
如果非同源,共有三种行为受到限制:
- Cookie、LocalStorage 和 IndexDB 无法读取。
- DOM 无法获得。
- AJAX 请求不能发送。
同源策略对于防范恶意页面是一种很好的防御机制,如果恶意脚本请求了非同源的一个资源,那么这种行为就很可能因为同源策略的限制被浏览器拒绝回复,从而在某种程度上缓解了攻击。虽然这些限制是必要的,但是有时很不方便,合理的用途也受到影响。因此有一些方法规避上面三种限制。
跨源的网络访问
现在知道了浏览器会根据同源策略允许或拒绝加载某些资源。可是想一想实际中的网页,就发现了问题:网站通常会将静态文件(css、js、图片)等放置在 CDN 上,那么 CDN 与当前域必然是不同源的。那么这是怎么回事呢?这就是跨源的网络访问。
页面跨域的行为主要会分为三类,分别是:
-
Cross-origin write,跨域写。通常被允许,例如链接,重定向和表单提交,一些不常见的HTTP请求方法例如 PUT、DELETE 等需要先发送预请求(preflight),例如发送OPTIONS来查询可用的方法。
-
Cross-origin embedding,跨域嵌入,通常被允许。
-
Cross-origin read,跨域读。通常被禁止,然而,我们可以用其他方法达到读取的效果。
一些可以跨域的方法:
具备src的标签
所有具有src属性的HTML标签都是可以跨域的,如 <script>
、<img>
、<iframe>
和 <link>
这几个标签是可以加载跨域的资源的,并且加载的方式其实相当于一次普通的 GET 请求,唯一不同的是,为了安全起见,浏览器不允许这种方式下对加载到的资源的读写操作,而只能使用标签本身应当具备的能力(比如脚本执行、样式应用等等)。
1 | # img标签跨域获取了一个图片资源,并且正常显示: |
JSONP
JSONP 跨域是一个非官方的协议。由于 <script>
标签是可以跨域的,而且在跨域脚本中可以直接回调当前脚本的函数,通过预先设定好的 callback 函数来实现和母页面的交互,通过 JSON 来传参,即将 JSON 数据填充进回调函数,这就是 JSONP 的 JSON with Padding 的含义。JSONP 只支持 GET 请求。
例如,前端:
1 | <script> |
上面代码通过动态添加 <script>
标签,向服务器发出请求。服务器收到这个请求以后,会将数据放在回调函数的参数位置返回。
后端:
1 |
|
由于 <script>
标签请求的脚本,直接作为代码运行。这时,只要浏览器定义了 work 函数,该函数就会立即调用。作为参数的 JSON 数据被视为 JavaScript 对象。
跨域资源共享
跨域资源共享(Cross Origin Resource Share,CORS),允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。CORS需要服务器端设置 Access-Control-Allow-Origin
头,否则浏览器会因为安全策略拦截返回的信息。
CORS 又分为简单跨域和非简单跨域请求。
简单请求
简单请求满足以下两个条件:
-
请求方法是以下三种方法之一:
- HEAD
- GET
- POST
-
HTTP 的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain
其他的都是非简单请求。
例如:
1 | <script> |
当浏览器判定此次请求是简单请求,会在请求头中增加一个 Origin
字段:
1 | Origin: http://www.test.com |
服务器根据 Origin
判断是否允许此次请求,如果允许会返回响应头:
1 | Access-Control-Allow-Origin: http://www.test.com |
其中:
-
Access-Control-Allow-Origin
该字段必须。它的值要么是请求时Origin字段的值,要么是一个*
,表示接受任意域名的请求。 -
Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示是否允许发送 Cookie。默认情况下,Cookie 不包括在 CORS 请求之中。若要发送 Cookie,AJAX 中必须设置xhr.withCredentials = true;
,而且 Access-Control-Allow-Origin 不能为星号。Cookie 依然遵循同源策略。 -
Access-Control-Expose-Headers
该字段可选。CORS 请求时,XMLHttpRequest 对象的 getResponseHeader() 方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在 Access-Control-Expose-Headers 里面指定。
如果服务器会不允许,则返回一个正常的 HTTP 回应。浏览器发现就知道错了,抛出一个错误。
非简单请求
非简单请求的 CORS,浏览器会在正式通信之前,增加一次 HTTP 查询请求,称为"预检"请求(preflight)。
例如:
1 | <script> |
浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,请求头:
1 | Origin: http://www.test.com |
服务器收到"预检"请求以后,检查了 Origin、Access-Control-Request-Method 和 Access-Control-Request-Headers 字段以后,允许跨源请求,则响应头:
1 | Access-Control-Allow-Origin: http://www.test.com |
其中 Access-Control-Allow-Methods 和 Access-Control-Allow-Headers 不限于浏览器在"预检"中请求的字段,表明服务器支持的所有跨域请求的方法、方法。
如果服务器会不允许,则返回一个正常的 HTTP 回应。浏览器就知道错了,抛出一个错误。
一旦服务器通过了"预检"请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样。
window.name
对当前窗口的 window.name 赋值,没有特殊的字符限制。由于 window 对象是浏览器的窗体,而非 document 对象,因此很多时候window 对象不受同源策略的影响。可以利用它,实现跨域、跨页面传递数据。
例如:
www.aaa.com/index.html 的代码为:
1 | <script> |
www.bbb.com/index.html 的代码为:
1 | <script> |
可以看到数据已经通过 window.name 跨域了。
document.domain
相同主域名不同子域名下的页面,可以设置 document.domain 让它们同域。
1 | <iframe id = "iframe" src="http://b.test.com"></iframe> |
1 | <script> |
location.hash
location.hash 属性是一个可读可写的字符串,该字符串是 URL 的锚部分(从 # 号开始的部分)。
利用 location.hash 来进行传值,父窗口可以把信息写入子窗口,子窗口也可以写入父窗口。
例如,www.aaa.com 下的 a.php 想和 www.bbb.com 下的 b.php 通信,但是由于同源策略的限制他们无法进行交流(b.php无法返回数据),于是就需要一个中间人:www.aaa.com 下的 proxy.php。b.php 将数据传给 proxy.php,由于 proxy.php 和 a.php 同源,于是可通过 proxy.php 将返回的数据传回给 a.php,从而达到跨域的效果。
www.aaa.com 下的 a.php:
1 | <script> |
www.bbb.com 下的 b.php:
1 | <script> |
www.aaa.com 下的 proxy.php
1 | <script> |
打开 http://www.aaa.com#100 ,就会返回 100**2+1 = 10001。
window.postMessage
HTML5 新增的 postMessage 方法,通过 postMessage 来传递信息,对方可以通过监听 message 事件来监听信息。可跨主域名及双向跨域。
postMessage 的使用方法: otherWindow.postMessage(message, targetOrigin)
- otherWindow:目标窗口,给哪个 window 发消息,是 window.frames 属性的成员或者由 window.open 方法创建的窗口
- message:是要发送的消息,类型为 String、Object (IE8、9 不支持)
- targetOrigin:是限定消息接收范围,不限制使用
*
例如:
www.aaa.com:
1 | <iframe id="bbb" src="http://www.bbb.com" onload="postMsg()"></iframe> |
1 | <script> |
成功接收到消息: