Web 站点内容安全策略(CSP)入门

梦康 2020-03-10 00:00:00 755

假如我们提供的是一个存储型的服务,比如博客。
案例 http://mengkang.net/demo/csp/1.php

<html>
<header>
    <title></title>
</header>
<body>
<div id="blog">
    <script>
        alert(document.cookie);
    </script>
</div>
</body>
</html>

也就是说"#blog"里面的内容是不可信的,一方面要做好展示时的过滤,假如我们没做好过滤,上面的 js 就会执行;另一方面,我们也不能随意修改用户的内容,所以即使展示时过滤了,而原始内容不能修改,当编辑用户的内容时,依然会触发xss,也就是self-xss,这主要攻击的就是有编辑其他人内容的人。

CSP 主要是为了解决跨站脚本攻击和数据注入攻击,它的核心原理是在服务端渲染页面的时候 http header 头里带上 CSP 协议,协议里面有一个nonce(服务端随机生成的码,不需要存储),然后页面需要执行的 js 必须也必须带上该nonce

拦截攻击

案例 http://mengkang.net/demo/csp/2.php

<?php
$nonce  = md5(uniqid());
$policy = "script-src 'nonce-" . $nonce . "';";

header("content-security-policy:". $policy);
?>
<html>
<header>
    <title></title>
</header>
<body>
<div id="blog">
    <script>
        alert(document.cookie);
    </script>
</div>
</body>
</html>

这样,用户写的博客里面的 js 就不会被执行了。并且控制台上会有报错记录

Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'nonce-be6737d35f2c558943dae9ad44c369ee'". Either the 'unsafe-inline' keyword, a hash ('sha256-0FWdMKk2PWuytGHwpB/r+HwqVTWZoSWX/M+OKSueWHI='), or a nonce ('nonce-...') is required to enable inline execution.

但是如果页面有一些我们自己开发的 js 要执行怎么办?

内部引入 js 的放行

案例 http://mengkang.net/demo/csp/3.php

<?php
$nonce  = md5(uniqid());
$policy = "script-src 'nonce-" . $nonce . "';";

header("content-security-policy:". $policy);
?>
<html>
<header>
    <title></title>
</header>
<body>
<div id="blog">
    <script>
        alert(document.cookie);
    </script>
</div>
<div id="notice">
    <script nonce="<?php echo $nonce?>">
        alert("我是自己人,别拦截");
    </script>
</div>
</body>
</html>

原理就是我们自己的 js script 标签上加上了服务端渲染的nonce值,所以能正常执行;而攻击者写的博客里面的js是写死的,即使他写了一个 nonce,但是他没法控制“被攻击者”打开页面时的nonce值正好等于他写死的那个值。

外部引入 js 的放行

案例 http://mengkang.net/demo/csp/4.php

<?php
$nonce  = md5(uniqid());
$policy = "script-src 'nonce-" . $nonce . "';";

header("content-security-policy:". $policy);
?>
<html>
<header>
    <title></title>
</header>
<body>
<div id="blog">
    <script>
        alert(document.cookie);
    </script>
</div>
<div id="notice">
    <script nonce="<?php echo $nonce?>">
        alert("我是自己人,别拦截");
    </script>
</div>
<script src="test.js"></script>
</body>
</html>

test.js是不能被引入的,需要改造成

<script src="test.js" nonce="<?php echo $nonce?>"></script>

如果外部引入 js 有动态插入的情况,一定要把nonce传递给外部 js。比如test.js背后是一段服务端代码,里面会执行document.write,解决方案是

<script src="test.js?nonce=<?php echo $nonce?>" nonce="<?php echo $nonce?>"></script>

test.js在收到参数nonce 之后,对后面动态插入的js做类似上面的操作两类操作添加nonce

行内 js 改造

如果页面中有大量的行内 js,需要改造成内部引入 js 的方式

<span onclick="alert(1);"></span>

这样的js 是不能被执行的,需要改造成

<span id="myButton"></span>

<script nonce="<?php echo $nonce?>">
    document.getElementById("myButton").addEventListener("click", myFunction);
    function myFunction(){
        alert(1);
    }
</script>

最终版

加上各种浏览器的兼容性,还有CSP1和CSP2的兼容性之后得到以下经过实战项目的规则

content-security-policy: base-uri 'self';script-src 'self' 'unsafe-inline' 'unsafe-eval' 'report-sample' https: http: 'strict-dynamic' 'nonce-543c368ef6f944e1a93fa60d39687a02';frame-src 'self' gaic.alicdn.com g.alicdn.com ;worker-src blob: 'self' data:;object-src 'self' g.alicdn.com;frame-ancestors *.aliyun.com;report-uri /csp/report;

可以参考这个去改下

  • frame-src控制我们可以引入的页面域名
  • frame-ancestors控制谁可以引用我们
  • report-uri是 csp 拦截日志上报地址