Fork me on GitHub

浅析web浏览器安全

最近在学习web浏览器安全,感觉对以前零散的安全策略需要有个比较系统的认识,所以写下这篇文章。根据近来的理解,先概括一下CORS和CSP的区别和联系。CSP是从客户端规定了可以加载从哪些域来的哪些资源,CORS从服务端规定了什么域下的客户端可以访问本域内的资源。二者从两个维度保证了浏览器的安全。

一.同源策略(Same Origin Policy)

传送门 github
说到浏览器安全,就绕不过同源策略,无论书从安全性还是用户体验上来说。它通过域名地址协议端口等为标志,规定了哪些网页属于同源。那么规定同源有什么用?同源的规定,使得一些来自不同源的“document”或者脚本。对当前“document”读取或者设置修改某些属性。(这句话参考了《白帽子讲web安全》)。这样非同源的网页间就不可以进行任意读写操作。这样说来还是比较拗的,简单的来说就是它阻止了B网页从A源加载的js去修改A源的页面,B引用的js只对当前网页B进行操作。这样一来,在逻辑层面上可以避免诸多的安全问题,同时也使得网页间变得有序,一定程度上防止了页面被非同源随意窜改,增加了安全性和用户体验。举个xss攻击的例子来说,假如用户正在访问多个页面,其中一个页面存在xss(假设攻击payload是获取cookies),那么用户只有该页面的cookies会被非法窃取,而同样打开的其他页面就不会有影响。

  • 同源策略对以下三个方面进行限制
    1
    2
    3
    4
    5
    (1) Cookie、LocalStorage 和 IndexDB 无法读取。

    (2) DOM 无法获得。

    (3) AJAX 请求不能发送。

二.浏览器CSP策略(Content Security Policy)

传送门 wiki
传送门 csp策略
传送门 内容安全政策
传送门 一个在线CSP检测和预警网站
CSP策略简单的来说就是一套浏览器提供给服务器端使用的安全策略或者说是协议吧,而这协议是浏览器所共同遵守的。只要服务器端在html代码中加入如下内容。

1
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';">

或者让服务器在http的返回头部返回一个请求头

Content-Security-Policy: default-src 'self'

浏览器就会遵循给定的策略去执行。比如说如果服务器所认同的源为本页源,那么就不会去执行或者加载外链的任何资源,包括css,html,图片,js等等,所以csp可以被看成预防xss的很好手段。

下面的策略如果没有特别指明是默认不会继承default-src的策略的

1
2
3
4
5
6
base-uri
form-action
frame-ancestors
plugin-types
report-uri
sandbox

三.Forbidden header name

https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name

所谓的forbidden header name指的是不允许编程语言操作的请求头,也就是说这部分请求头的产生是由浏览器来进行的。JS中的xmlhttprequest是无法操作的,所以说在进行token识别与验证的时候采用这部分来作为标识是相对安全的,如一些以sec开头的字段。

PS: 这只是说在基于浏览器运行的脚本语言无法操作这些请求头,但是不代表其他抓包工具例如burpsuit无法进行修改。但在xss,csrf的防御上,这一点可以很好的实现对用户保护。除非再使用中间人攻击,否则攻击不会直接生效。

四.轮询机制

在一些网页需要定时与服务器进行通讯的情景,轮询机制就会发生作用。

1.短沦陷

所谓的短轮询,指的就是每次轮询都进行完整头部请求,也就是说服务器不会hold住这个链接,每次都相当于客户端与服务器重新链接。这样的好处就是能够比较好的保证客户端数据获取的时效性和次序一致。缺点就是每次都发送完整的头部到服务器会增加额外的消耗。

2.长轮询

长连接就是服务器会hold住该次链接,在满足不断开条件下服务器hold到下一次客户端的轮询来临。

五.CORS内容

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

CORS主要用来实现跨域的需求。我们知道浏览器默认是不允许跨域资源共享的,但是在某些场合,如静态资源的共享上我们需要允许跨域访问,如jquery,bootstrap等静态库支持远程调用,在这种场合下CORS就应运而生。CORS的实现是在服务端的请求头部返回权限相关控制头部,大致有如下几项:

1
2
3
4
5
6
1. Access-Control-Allow-Origin
2. Access-Control-Expose-Headers
3. Access-Control-Max-Age
4. Access-Control-Allow-Credentials
5. Access-Control-Allow-Methods
6. Access-Control-Allow-Headers

六.规避同源策略的手段

1. 所有带src或href属性的标签以及部分其他标签可以跨域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script src="..."></script>
<img src="...">
<video src="..."></video>
<audio src="..."></audio>
<embed src="...">
<frame src="...">
<iframe src="..."></iframe>
<link rel="stylesheet" href="...">
<applet code="..."></applet>
<object data="..." ></object>
@font-face可以引入跨域字体。
<style type="text/css">
@font-face {
src: url("http://developer.mozilla.org/@api/deki/files/2934/=VeraSeBd.ttf");
}
</style>

除了之前提到js标签可以跨域之外,开发中也有一些常用的手段实现跨域资源访问。

2. document.domain

  • IE8以前的同源策略是可以被直接修改的

    1
    2
    3
    4
    var document;
    document = {};
    document.domain = 'http://www.a.com';
    console.log(document.domain);
  • document.domain 只可以被设置为他的当前域或其当前域的父域

    1
    2
    3
    4
    /*假设本案例中的domain为:aaa.domain1.com,那么尝试如下的修改,结果如下*/
    document.domain = 'http://domain1.com'; //right
    document.domain = 'http://bbb.domain1.com';//error
    document.domain = 'http://domain2.com';//error
  • document.domain 的赋值操作会导致端口号被重写为NULL,所以 aaa.evoa.me 仅设置document.domain为evoa.me 并不能与evoa.me进行通信,evoa.me的页面也必须赋值一次使双方端口相同从而通过浏览器的同源检测。这么做的目的是,如果子域有XSS,那么他的父域都存在安全隐患。

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

3. window.name+iframe

window.name字iframe窗体中是共享的,这样一来,我们完全可以在iframe中加载一个域dom,然后把需要跨域的资源传递给window.name,如此一来,我们修改iframe窗体的src,此时的src与之前的src可以完全不同源,但是仍旧可以访问之前window.name的值。下面具体说下演示环境的搭建。

  • 修改本机host解析

    1
    2
    3
    4
    127.0.0.1	localhost
    127.0.1.1 ubuntu
    127.0.0.1 d1.domain1.com
    127.0.0.1 d2.domain.com
  • apache2 配置不同源两个虚拟host /etc/apache2/sites-available/000-default.conf

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <VirtualHost *:80>
    ServerName d1.domain1.com
    DocumentRoot /var/www/html/domain1
    </VirtualHost>

    <VirtualHost *:8080>
    ServerName d2.domain.com
    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/html/domain2
    </VirtualHost>
  • d1.domain1.com 目录文件

    1
    2
    3
    4
    5
    6
    7
    // index.html
    <html>
    <h1>hello doamin1</h1>
    <script>
    window.name = "domain1";
    </script>
    </html>
  • d2.domain.com 目录文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // index.html
    <html>
    <iframe id='iframe' src="http://d1.domain1.com/index.html"></iframe>
    <script>
    setTimeout(function(){ iframe.src="http://d2.domain.com:8080/js.html";}, 3000);

    </script>
    </html>

    // js.html
    <html>
    <script> alert(window.name);</script>
    </html>

我们访问:http://d2.domain.com:8080/index.html可以发现弹窗。

image

4. location.hash+iframe

location hash其实也就是我们平时所说的锚点,不同的iframe之间是可以通过url锚点,也即#后面的信息是可以作为跨域资源访问共享的。演示起来也十分简单,我们可以在上述演示环境的基础上,稍微修改下html的界面,就可以。修改如下:

  • d1.domain1.com

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // index.html
    <html>
    <body>
    </body>
    <script>
    console.log(location.hash);
    //window.parent.location.hash = "from domain1";
    let iframe = document.createElement('iframe');
    iframe.src = 'http://d2.domain.com:8080/child.html#from_domain1';
    document.body.appendChild(iframe);
    </script>
    </html>
  • d2.domain.com

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // index.html
    <iframe src="http://d1.domain1.com/index.html#fromdomain2"></iframe>
    <script>
    window.onhashchange = function () { //检测hash的变化
    alert(location.hash);
    }
    </script>

    // child.html
    <script>
    window.parent.parent.location.hash = location.hash
    //b.html将结果放到a.html的hash值中,b.html可通过parent.parent访问a.html页面
    </script>

image

我们可以看到,domain2和domain1通信只需要将值fromdomian2放到location.hash就可以,domain1想要和domain2通信却需要继续内嵌一层与domain2同源的iframe,否则会报非同源访问错误。要想直接实现非同源子iframe与父窗通信,可以采用下面说到的Postmessage。

5. PostMessage

PostMessage可以实现真正意义上的跨域,它可以在窗口和弹出窗口之间,或窗口和iframe之间跨域通信。

  • domain1

    1
    2
    3
    4
    # index.html
    <script>
    parent.postMessage('from domain1','*');
    </script>
  • domain2

    1
    2
    3
    4
    5
    6
    7
    # index.html
    <iframe id='iframe' src="//d1.domain1.com/postmessage/index.html"></iframe>
    <script>
    window.addEventListener('message',function(e){
    alert(e.data);
    })
    </script>

image

6. jsonp

jsonp是一种以往比较常用的跨域手段。其原理比较绕,涉及服务器和客户端的交互过程。

  • 客户端

    1
    2
    3
    4
    5
    6
    <script>
    function alertData(data) {
    alert(data.name);
    }
    </script>
    <script src="//d2.domain.com:8080/jsonp/server.php?func=alertData"></script>
  • 服务端

    1
    2
    3
    4
    5
    6
    <?php
    header('Content-type: application/javascript');
    $func = $_REQUEST['func'];
    $data = '{"name": "lily"}';
    echo $func . "(" . $data . ")";
    ?>

image

需要注意的是,使用这种跨域方式,对客户端的输入数据过滤不严容易造成xss。以这题为例,加入我们在服务端注释header那一行编码,在客户端直接访问http://d2.domain.com:8080/jsonp/server.php?func=%3Cscript%3Ealert(/xss/)%3C/script%3E//就会有xss。

image

7. LocalStorage

html5标准中一个亮点就是提供了浏览器本地存储的功能。方式有两种:localStorage和 sessionStorage。 相对于cookie,他们具有存储空间大的特点,一般可以存储5M左右,而cookie一般只有4k。但是在生产实际中,往往需要实现LocalStorage的跨域访问,其实本质上实现LocalStorage跨域访问的手段是调用PostMessage的方式,由于之前也有具体说明,这里就不打算细说。

8. websocket

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯。正是这样一种新的特性存在,csrf结合websocket出现了一种叫做websocket跨域劫持(CSWSH)的漏洞,危害巨大,不但具有csrf的攻击危害,还可以泄露内部数据,甚至修改内部数据。

详细:https://www.ibm.com/developerworks/cn/java/j-lo-websocket-cross-site/index.html

下面是访问字节跳动校招网站的websocket客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
var ws = new WebSocket("wss://job.bytedance.com/user/profile/");

ws.onopen = function(evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};

ws.onmessage = function(evt) {
console.log( "Received Message: " + evt.data);
ws.close();
};

ws.onclose = function(evt) {
console.log("Connection closed.");
};
</script>

image

image

对比正常访问的请求头和websocket访问的请求头,我们发现websocket在非同源页面请求貌似不会自动填充cookie字段,如果服务端也没有做cookie校验,那么我们可以直接伪造发送跨域;如果服务端有校验,我们通过XSS手段拿到cookie进行请求伪造;所以正确的防御姿势是校验cookie,同时校验origin头部,cookie字段加入httponly,适当的可以加入token机制(类似scrf),让websocket发起链接时使用一个hash token值。

9.navigation

要求:IE6/7
原理:frame之间的window.navigator对象是共享的,就如同windows.name和location.hash。只不过这种方式利用具有一定的局限性质。

10.nginx中间件服务端转发

注意到同源策略只限制在客户端浏览器,所以使用nginx作为中间件进行请求转发,无浏览器参与,故没有同源限制,是完全可以实现跨域。
nginx proxy服务器配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;

# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}

如此一来我们通过客户端Ajax请求同域内的代理服务器www.domain1.com就可以访问www.domain2.com:8080的服务,实现了Ajax的跨域。

参考

[1] https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=Same+Origin+Policy+bypass
[2] http://www.yilan.io/article/5c999d628c9d600827a58f13

-------------本文结束感谢您的阅读-------------