目录

    • 1、概述
    • 2、同源策略
      • 2.1、同源策略是什么
      • 2.2、同源策略的目的
    • 3、跨域解决方法
      • 3.1、请求跨域解决方法
        • 3.1.1、CORS(跨域资源共享)
        • 3.1.2、JSONP跨域
        • 3.1.3、WebSocket跨域
        • 3.1.4、请求代理
      • 3.2、页面跨域解决方法
        • 3.2.1、postMessage
        • 3.2.2、document.domain跨域
        • 3.2.3、window.name跨域
        • 3.2.4、location.hash跨域
    • 4、内容安全策略(CSP)
      • 4.1、CSP使用方式
      • 4.2、CSP语法
      • 4.3、CSP指令
      • 4.4、CSP指令值
      • 4.5、CSP例子
      • 4.6、CSP默认特性
      • 4.7、CSP分析报告

1、概述

最近,使用APPSCAN扫描系统时,扫描出存在"Content-Security-Policy"头缺失漏洞。

提到CSP,就不能不提到浏览器同源策略(same-origin-policy)及跨域解决方法。

2、同源策略

2.1、同源策略是什么

1995年,同源策略由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个策略。

最初,它的含义是指,A网页设置的 Cookie,B网页不能打开,除非这两个网页"同源"。所谓"同源"指的
是"三个相同"。

协议相同
域名相同
端口相同

举个例子,我们以http://storepany/dir/page.html为例,跟下面表格的url做对比。

URL结果原因
http://storepany/dir2/other.html同源只有路径不同
http://storepany/dir/inner/another.html同源只有路径不同
https://storepany/secure.html不同源协议不同
http://storepany:81/dir/etc.html不同源端口不同
http://newspany/dir/other.html不同源域名不同

同源策略可分为以下两种情况:

1、DOM 同源策略:禁止对不同源页面 DOM 进行操作。这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的。
2、XMLHttpRequest 同源策略:禁止使用 XHR 对象向不同源的服务器地址发起 HTTP 请求。

2.2、同源策略的目的

因为存在浏览器同源策略,所以才会有跨域问题。那么浏览器是出于何种原因会有跨域的限制呢。其实不难想到,跨域限制主要的目的就是为了用户的上网安全。

如果浏览器没有同源策略,会存在什么样的安全问题呢。下面从 DOM 同源策略和 XMLHttpRequest 同源策略来举例说明:

一、如果没有 DOM 同源策略,也就是说不同域的 iframe 之间可以相互访问,那么黑客可以这样进行攻击:
1、做一个假网站,里面用 iframe 嵌套一个银行网站 http://mybank。
2、把 iframe 宽高啥的调整到页面全部,这样用户进来除了域名,别的部分和银行的网站没有任何差别。
3、这时如果用户输入账号密码,我们的主网站可以跨域访问到 http://mybank 的 dom 节点,就可以拿到用户的账户密码了。

二、如果没有 XMLHttpRequest 同源策略,那么黑客可以进行 CSRF(跨站请求伪造) 攻击:
1、用户登录了自己的银行页面 http://mybank,http://mybank 向用户的 cookie 中添加用户标识。
2、用户浏览了恶意页面 http://evil,执行了页面中的恶意 AJAX 请求代码。
3、http://evil 向 http://mybank 发起 AJAX HTTP 请求,请求会默认把 http://mybank 对应 cookie 也同时发送过去。
4、银行页面从发送的 cookie 中提取用户标识,验证用户无误,response 中返回请求数据。此时数据就泄露了。
5、而且由于 Ajax 在后台执行,用户无法感知这一过程。

3、跨域解决方法

因为有了同源策略的作用,跨域被限制了,我们才能安全上网,但实际业务中部分合理的需求,又需要跨域,所以才产生了跨域的解决方法。

根据同源策略限制的2种类型,又可分为2种类型的跨域解决方法。
一、请求跨域

CORS
JSONP
WebSocket
代理转发

二、页面跨域

postMessage
document.domain
window.name
location.hash

3.1、请求跨域解决方法

3.1.1、CORS(跨域资源共享)

CORS(Cross-origin resource sharing,跨域资源共享)是一个 W3C 标准,定义了在必须访问跨域资源时,浏览器与服务器应该如何沟通。是浏览器为 AJAX 请求设置的一种跨域机制,让其可以在服务端允许的情况下进行跨域访问。主要通过 HTTP 响应头来告诉浏览器服务端是否允许当前域的脚本进行跨域访问。

CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10。

整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

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

1、请求方法是以下三种方法之一:
HEAD
GET
POST

2、HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain

凡是不同时满足上面两个条件,就属于非简单请求。

浏览器对这两种请求的处理,是不一样的。

1、在请求中需要附加一个额外的 Origin 头部,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给予响应。例如:Origin: http://www.laixiangran
2、如果服务器认为这个请求可以接受,就在 Access-Control-Allow-Origin 头部中回发相同的源信息(如果是公共资源,可以回发 * )。例如:Access-Control-Allow-Origin:http://www.laixiangran
3、没有这个头部或者有这个头部但源信息不匹配,浏览器就会驳回请求。正常情况下,浏览器会处理请求。注意,请求和响应都不包含 cookie 信息。
4、如果需要包含 cookie 信息,ajax 请求需要设置 xhr 的属性 withCredentials 为 true,服务器需要设置响应头部 Access-Control-Allow-Credentials: true

非简单请求
浏览器在发送真正的请求之前,会先发送一个 Preflight 请求给服务器,这种请求使用 OPTIONS 方法,发送下列头部:

Origin:与简单的请求相同。
Access-Control-Request-Method: 请求自身使用的方法。
Access-Control-Request-Headers: (可选)自定义的头部信息,多个头部以逗号分隔。
例如

Origin: http://www.laixiangran
Access-Control-Request-Method: POST
Access-Control-Request-Headers: NCZ

发送这个请求后,服务器可以决定是否允许这种类型的请求。服务器通过在响应中发送如下头部与浏览器进行沟通:

Access-Control-Allow-Origin:与简单的请求相同。
Access-Control-Allow-Methods: 允许的方法,多个方法以逗号分隔。
Access-Control-Allow-Headers: 允许的头部,多个方法以逗号分隔。
Access-Control-Max-Age: 应该将这个 Preflight 请求缓存多长时间(以秒表示)。

一旦服务器通过 Preflight 请求允许该请求之后,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样了。

优点:
1、CORS 通信与同源的 AJAX 通信没有差别,代码完全一样,容易维护。
2、支持所有类型的 HTTP 请求。
缺点:
1、存在兼容性问题,特别是 IE10 以下的浏览器。
2、第一次发送非简单请求时会多一次请求。

3.1.2、JSONP跨域

由于 script 标签不受浏览器同源策略的影响,允许跨域引用资源。因此可以通过动态创建 script 标签,然后利用 src 属性进行跨域,这也就是 JSONP 跨域的基本原理。

直接通过下面的例子来说明 JSONP 实现跨域的流程:

// 1. 定义一个 回调函数 handleResponse 用来接收返回的数据
function handleResponse(data) {
    console.log(data);
};

// 2. 动态创建一个 script 标签,并且告诉后端回调函数名叫 handleResponse
var body = document.getElementsByTagName('body')[0];
var script = document.gerElement('script');
script.src = 'http://www.laixiangran/json?callback=handleResponse';
body.appendChild(script);

// 3. 通过 script.src 请求 `http://www.laixiangran/json?callback=handleResponse`,
// 4. 后端能够识别这样的 URL 格式并处理该请求,然后返回 handleResponse({"name": "laixiangran"}) 给浏览器
// 5. 浏览器在接收到 handleResponse({"name": "laixiangran"}) 之后立即执行 ,也就是执行 handleResponse 方法,获得后端返回的数据,这样就完成一次跨域请求了。

优点
使用简便,没有兼容性问题,目前最流行的一种跨域方法。

缺点
只支持 GET 请求。
由于是从其它域中加载代码执行,因此如果其他域不安全,很可能会在响应中夹带一些恶意代码。
要确定 JSONP 请求是否失败并不容易。虽然 HTML5 给 script 标签新增了一个 onerror 事件处理程序,但是存在兼容性问题。

3.1.3、WebSocket跨域

Websocket 是 HTML5 规范提出的一个应用层的全双工协议,适用于浏览器与服务器进行实时通信场景。

全双工通信传输的一个术语,这里的“工”指的是通信方向。

“双工”是指从客户端到服务端,以及从服务端到客户端两个方向都可以通信,“全”指的是通信双方可以同时向对方发送数据。与之相对应的还有半双工和单工,半双工指的是双方可以互相向对方发送数据,但双方不能同时发送,单工则指的是数据只能从一方发送到另一方。

下面是一段简单的示例代码。在 a 网站直接创建一个 WebSocket 连接,连接到 b 网站即可,然后调用 WebScoket 实例 ws 的 send() 函数向服务端发送消息,监听实例 ws 的 onmessage 事件得到响应内容。

let ws = new WebSocket("ws://b");
ws.onopen = function(){
  // ws.send(...);
}
ws.onmessage = function(e){
  // console.log(e.data);
}

3.1.4、请求代理

我们知道浏览器有同源策略的安全限制,但是服务器没有限制,所以我们可以利用服务器进行请求转发。

以 webpack 为例,利用 webpack-dev-server 配置代理, 当浏览器发起前缀为 /api 的请求时都会被转发到 http://localhost:3000 服务器,代理服务器将获取到响应返回给浏览器。对于浏览器而言还是请求当前网站,但实际上已经被服务端转发。

// webpack.config.js
module.exports = {
  //...
  devServer: {
    proxy: {
      '/api': 'http://localhost:3000'
    }
  }
};

// 使用 Nginx 作为代理服务器
location /api {
    proxy_pass   http://localhost:3000;
}

3.2、页面跨域解决方法

请求跨域之外,页面之间也会有跨域需求,例如使用 iframe 时父子页面之间进行通信。常用方案如下:

postMessage
document.domain
window.name(不常用)
location.hash + iframe(不常用)

3.2.1、postMessage

window.postMessage(message,targetOrigin) 方法是 HTML5 新引进的特性,可以使用它来向其它的 window 对象发送消息,无论这个 window 对象是属于同源或不同源。这个应该就是以后解决 dom 跨域通用方法了。
调用 postMessage 方法的 window 对象是指要接收消息的那一个 window 对象,该方法的第一个参数 message 为要发送的消息,类型只能为字符串;第二个参数 targetOrigin 用来限定接收消息的那个 window 对象所在的域,如果不想限定域,可以使用通配符 *。
需要接收消息的 window 对象,可是通过监听自身的 message 事件来获取传过来的消息,消息内容储存在该事件对象的 data 属性中。
页面 http://www.laixiangran/a.html 的代码:

<iframe src="http://laixiangran/b.html" id="myIframe" onload="test()" style="display: none;">
<script>
    // 1. iframe载入 "http://laixiangran/b.html 页面后会执行该函数
    function test() {
        // 2. 获取 http://laixiangran/b.html 页面的 window 对象,
        // 然后通过 postMessage 向 http://laixiangran/b.html 页面发送消息
        var iframe = document.getElementById('myIframe');
        var win = iframe.contentWindow;
        win.postMessage('我是来自 http://www.laixiangran/a.html 页面的消息', '*');
    }
</script>

页面 http://laixiangran/b.html 的代码:

<script type="text/javascript">
    // 注册 message 事件用来接收消息
    window.onmessage = function(e) {
        e = e || event; // 获取事件对象
        console.log(e.data); // 通过 data 属性得到发送来的消息
    }
</script>

3.2.2、document.domain跨域

对于主域名相同,而子域名不同的情况,可以使用 document.domain 来跨域。这种方式非常适用于 iframe 跨域的情况。

比如,有一个页面,它的地址是 http://www.laixiangran/a.html,在这个页面里面有一个 iframe,它的 src 是 http://laixiangran/b.html。很显然,这个页面与它里面的 iframe 框架是不同域的,所以我们是无法通过在页面中书写 js 代码来获取 iframe 中的东西的。

这个时候,document.domain 就可以派上用场了,我们只要把 http://www.laixiangran/a.htmlhttp://laixiangran/b.html这两个页面的 document.domain 都设成相同的域名就可以了。但要注意的是,document.domain 的设置是有限制的,我们只能把 document.domain 设置成自身或更高一级的父域,且主域必须相同。例如:a.b.laixiangran 中某个文档的 document.domain 可以设成 a.b.laixiangran、b.laixiangran 、laixiangran 中的任意一个,但是不可以设成 c.a.b.laixiangran ,因为这是当前域的子域,也不可以设成 baidu,因为主域已经不相同了。

例如,在页面 http://www.laixiangran/a.html 中设置document.domain:

<iframe src="http://laixiangran/b.html" id="myIframe" onload="test()">
<script>
    document.domain = 'laixiangran'; // 设置成主域
    function test() {
        console.log(document.getElementById('myIframe').contentWindow);
    }
</script>

在页面 http://laixiangran/b.html 中也设置 document.domain,而且这也是必须的,虽然这个文档的 domain 就是 laixiangran,但是还是必须显式地设置 document.domain 的值:

<script>
    document.domain = 'laixiangran'; // document.domain 设置成与主页面相同
</script>

这样,http://www.laixiangran/a.html 就可以通过 js 访问到 http://laixiangran/b.html 中的各种属性和对象了。

3.2.3、window.name跨域

window 对象有个 name 属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面(不管是相同域的页面还是不同域的页面)都是共享一个 window.name 的,每个页面对 window.name 都有读写的权限,window.name 是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。

通过下面的例子介绍如何通过 window.name 来跨域获取数据的。

页面 http://www.laixiangran/a.html 的代码:

<iframe src="http://laixiangran/b.html" id="myIframe" onload="test()" style="display: none;">
<script>
    // 2. iframe载入 "http://laixiangran/b.html 页面后会执行该函数
    function test() {
        var iframe = document.getElementById('myIframe');
        
        // 重置 iframe 的 onload 事件程序,
        // 此时经过后面代码重置 src 之后,
        // http://www.laixiangran/a.html 页面与该 iframe 在同一个源了,可以相互访问了
        iframe.onload = function() {
            var data = iframe.contentWindow.name; // 4. 获取 iframe 里的 window.name
            console.log(data); // hello world!
        };
        
        // 3. 重置一个与 http://www.laixiangran/a.html 页面同源的页面
        iframe.src = 'http://www.laixiangran/c.html';
    }
</script>

页面 http://laixiangran/b.html 的代码:

<script type="text/javascript">
    // 1. 给当前的 window.name 设置一个 http://www.laixiangran/a.html 页面想要得到的数据值 
    window.name = "hello world!";
</script>

3.2.4、location.hash跨域

location.hash 方式跨域,是子框架修改父框架 src 的 hash 值,通过这个属性进行传递数据,且更改 hash 值,页面不会刷新。但是传递的数据的字节数是有限的。
页面 http://www.laixiangran/a.html 的代码:

<iframe src="http://laixiangran/b.html" id="myIframe" onload="test()" style="display: none;">
<script>
    // 2. iframe载入 "http://laixiangran/b.html 页面后会执行该函数
    function test() {
        // 3. 获取通过 http://laixiangran/b.html 页面设置 hash 值
        var data = window.location.hash;
        console.log(data);
    }
</script>

页面 http://laixiangran/b.html 的代码:

<script type="text/javascript">
    // 1. 设置父页面的 hash 值
    parent.location.hash = "world";
</script>

4、内容安全策略(CSP)

内容安全策略(Content Security Policy,简称CSP)是一种以可信白名单作机制,来限制网站是否可以包含某些来源内容,缓解广泛的内容注入漏洞,比如 XSS。 简单来说,就是我们能够规定,我们的网站只接受我们指定的请求资源。默认配置下不允许执行内联代码(<script>块内容,内联事件,内联样式),以及禁止执行eval() , newFunction() , setTimeout([string], …) 和setInterval([string], …) 。

4.1、CSP使用方式

CSP可以由两种方式指定: HTTP Header 和 HTML。

通过定义在HTTP响应 header 中使用:

"Content-Security-Policy:" 策略集

通过定义在 HTML meta标签中使用:

<meta http-equiv="content-security-policy" content="策略集">

策略是指定义 CSP 的语法内容。

如果 HTTP 头 与 meta 标签同时定义了 CSP,则会优先采用 HTTP 头的 。

定义后,凡是不符合 CSP策略的外部资源都会被阻止加载。

4.2、CSP语法

策略
每一条策略都是指令与指令值组成:

Content-Security-Policy:指令1 指令值1

策略与策略之间用分号隔开,例如:

Content-Security-Policy:指令1 指令值1;指令2 指令值2;指令3 指令值3

在一条策略中,如果一个指令中有多个指令值,则指令值之间用空号隔开:

Content-Security-Policy:指令a 指令值a1 指令值a2

4.3、CSP指令

default-src : 定义针对所有类型(js/image/css/font/ajax/iframe/多媒体等)资源的默认加载策略,如果某类型资源没有单独定义策略,就使用默认的。
script-src : 定义针对 JavaScript 的加载策略。
style-src : 定义针对样式的加载策略。
img-src : 定义针对图片的加载策略。
font-src : 定义针对字体的加载策略。
media-src : 定义针对多媒体的加载策略,例如:音频标签和视频标签。
object-src : 定义针对插件的加载策略,例如:、、。
**child-src 😗*定义针对框架的加载策略,例如: ,。
connect-src : 定义针对 Ajax/WebSocket 等请求的加载策略。不允许的情况下,浏览器会模拟一个状态为400的响应。
sandbox : 定义针对 sandbox 的限制,相当于 的sandbox属性。
report-uri : 告诉浏览器如果请求的资源不被策略允许时,往哪个地址提交日志信息。
form-action : 定义针对提交的 form 到特定来源的加载策略。
referrer : 定义针对 referrer 的加载策略。
reflected-xss : 定义针对 XSS 过滤器使用策略。

4.4、CSP指令值

指令值说明
*允许加载任何内容
‘none’不允许加载任何内容
‘self’允许加载相同源的内容
www.a允许加载指定域名的资源
*.a允许加载 a 任何子域名的资源
https://a允许加载 a 的 https 资源
https:允许加载 https 资源
data:允许加载 data: 协议,例如:base64编码的图片
‘unsafe-inline’允许加载 inline 资源,例如style属性、onclick、inline js、inline css等
‘unsafe-eval’允许加载动态 js 代码,例如 eval()

4.5、CSP例子

eg1:
所有内容均来自网站的自己的域:

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

eg2:
所有内容都来自网站自己的域,还有其他子域(假如网站的地址是:a):

Content-Security-Policy:default-src 'self' *.a

eg3:
网站接受任意域的图像,指定域(a)的音频、视频和多个指定域(a、b)的脚本

Content-Security-Policy:default-src 'self';img-src *;media-src a;script-src a b

4.6、CSP默认特性

一、阻止内联代码执行
CSP除了使用白名单机制外,默认配置下阻止内联代码执行是防止内容注入的最大安全保障。
这里的内联代码包括:

<script>getyourcookie()</script>

(2) 内联事件。

<a href="" onclick="handleClick();"></a> 
<a href="javascript:handleClick();"></a>

(3) 内联样式

<div style="display:none"></div>

虽然CSP中已经对script-src和style-src提供了使用”unsafe-inline”指令来开启执行内联代码,但为了安全起见还是慎用”unsafe-inline”。

二、eval相关功能被禁用
用户输入字符串,然后经过eval()等函数转义进而被当作脚本去执行。这样的攻击方式比较常见。于是乎CSP默认配置下,eval() , newFunction() , setTimeout([string], …) 和setInterval([string], …)都被禁止运行。
比如:

alert(eval("foo.bar.baz"));
window.setTimeout("alert('hi')", 10); window.setInterval("alert('hi')", 10); 
new Function("return foo.bar.baz");

如果想执行可以把字符串转换为内联函数去执行。

alert(foo && foo.bar && foo.bar.baz);
window.setTimeout(function() { alert('hi'); }, 10);
window.setInterval(function() { alert('hi'); }, 10);
function() { return foo && foo.bar && foo.bar.baz };

同样CSP也提供了”unsafe-eval”去开启执行eval()等函数,但强烈不建议去使用”unsafe-eval”这个指令。

4.7、CSP分析报告

可以用report-uri指令使浏览器发送HTTP POST请求把攻击报告以JSON格式传送到你指定的地址。接下来给大家介绍你的站点如何配置来接收攻击报告。

启用报告

默认情况下,违规报告不会发送。为了能使用违规报告,你必须使用report-uri指令,并至少提供一个接收地址。

Content-Security-Policy: default-src 'self'; report-uri http://reportcollector.example/collector.cgi

如果想让浏览器只汇报报告,不阻止任何内容,可以改用Content-Security-Policy-Report-Only头。

违规报告语法
该报告JSON对象包含以下数据:

blocked-uri:被阻止的违规资源
document-uri:拦截违规行为发生的页面
original-policy:Content-Security-Policy头策略的所有内容
referrer:页面的referrer
status-code:HTTP响应状态
violated-directive:违规的指令

违规报告例子
http://example/signup.html 中CSP 规定只能加载cdn.example的CSS样式。

Content-Security-Policy: default-src 'none'; style-src cdn.example; report-uri /test/csp-report.php

signup.html中的代码类似与这样:

<!DOCTYPE html>
<html>
 <head>
   <title>Sign Up</title>
   <link rel="stylesheet" href="css/style.css">
 </head>
 <body>
   ... Content ...
 </body>
</html>

你能从上面的代码找出错误吗?策略是只允许加载cdn.example中的CSS样式。但signup.html试图加载自己域的style.css样式。这样违反了策略,浏览器会向 http://example/test/csp-report.php 发送POST请求提交报告,发送格式为JSON格式。

{
  "csp-report": {
    "document-uri": "http://example/signup.html",
    "referrer": "",
    "blocked-uri": "http://example/css/style.css",
    "violated-directive": "style-src cdn.example",
    "original-policy": "default-src 'none'; style-src cdn.example; report-uri /_/csp-reports",
  }
}

你从上面可以看到blocked-uri给出了详细的阻断地址 http://example/css/style.css,但也并不是每次都是这样。比如试图从 http://anothercdn.example/stylesheet.css 加载CSS样式时,浏览器将不会传送完整的路径,只会给出 http://anothercdn.example/ 这个地址。这样做是为了防止泄漏跨域的敏感信息。

服务端csp-report.php代码可以这样写:

<?php 
$file = fopen('csp-report.txt', 'a');
$json = file_get_contents('php://input');
$csp = json_decode($json, true);
foreach ($csp['csp-report'] as $key => $val) {
    fwrite($file, $key . ': ' . $val . "
");
}
fwrite($file, 'End of report.' . "
");
fclose($file);
?>

参考资料
https://juejin/post/6844903681683357710
https://www.ruanyifeng/blog/2016/04/same-origin-policy.html
https://segmentfault/a/1190000039690701
https://wwwblogs/mutudou/p/14373644.html

更多推荐

由“Content-Security-Policy“头缺失引起的总结