两年前,我写过一篇介绍 Content Security Policy(CSP)的文章(详情),CSP 是一个用来定义页面可以加载或执行哪些资源的协议,目前已经发展到了 Level 2(协议地址)。我在本站之前的文章中已经多次提到过 CSP2,这篇文章也早就躺在我的草稿箱,只是断断续续写了好久才写完。
CSP2 基本兼容 CSP1 指令,但有一些变化和新的指令,汇总如下:
child-src
指令控制,而不是 script-src
;另外,CSP1 中 Worker 的加载策略由父页面决定,而 CSP2 新增了专门针对 Worker 的策略;base-uri
指令,用于指定页面的 base URL;child-src
;同时废弃了 CSP1 中用于 frame 的 frame-src
指令;form-action
指令,用于指定表单提交 URL 白名单;frame-ancestors
指令,用于指定页面被其他页面嵌入时的行为。(后面会详细介绍);plugin-types
指令,用于指定页面允许加载的插件类型;SecurityPolicyViolationEvent
事件;SecurityPolicyViolationEvent
事件,或者通过 report-uri
指令指定了上报地址时,增加了更多字段信息便于定位问题;通常,在支持 CSP2 的浏览器中使用 CSP1 规则,功能上不会有什么变化,但某些极端情况下也会出问题。我们来看一个案例:
Content-Security-Policy: default-src 'none'; script-src 'self'
在支持 CSP1 的浏览器中,以上规则允许加载本域的 Web Worker(由 script-src
控制);而在支持 CSP2
的浏览器中,所有 Worker 文件都无法加载(CSP2 中 Worker 改由 child-src
控制,没有指定 child- src
时,使用 default-src
指定的 none
规则)。
CSP2 的浏览器支持情况可以通过 CanIUse 查看。目前主流浏览器中,Chrome 和 Firefox 对 CSP2 支持得比较好,IE 和
Safari 比较落后。所以现阶段即便使用了 CSP2,frame-src
这样的 CSP1 指令依然要保留。
很多 HTTP 响应头都有对应的 <meta>
形式,CSP 也不例外。例如以下二者是等价的:
Content-Security-Policy: script-src 'nonce-1'
'sha256-qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng='
<meta http-equiv="Content-Security-Policy" content="script-src 'nonce-1'
'sha256-qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng='">
但有几点需要注意:1)report-uri
、frame-ancestors
、sandbox
这三个指令在通过
<meta>
指定的 CSP 规则中无效;2)CSP <meta>
标签必须位于 </head>
之前,否则无效;3)迄今为止,Firefox 仍不支持通过 <meta>
标签指定 CSP 策略(详情)。所以推荐使用响应头来设置 CSP
规则。
在 CSP1 时代,我们对 inline 资源能采取的策略比较单一,那么全部不允许,要么全部允许。CSP 的目标之一是减少 XSS,如果网站主能够禁用
inline script,对 XSS 确实有致命打击,但这并不现实,太多的网站还是会启用 unsafe-inline
和 unsafe- eval
,让这道防线形同虚设。
CSP2 考虑到这种现状,允许给 inline 脚本和样式配置更为精确的规则,这就是 Nonces
和
Hashes
。它们都可以用来确保仅执行已知的 inline 代码,避免 XSS 代码运行。
Nonces 是通过在 CSP 中指定允许资源的序号,达到限制非法 inline 代码的目的。只有与 CSP 策略中序号一致的代码可以执行,具体使用方式如下:
Content-Security-Policy: script-src 'nonce-1'
<script nonce="1">alert(0);</script>
实际使用中为了确保安全,nonce
的值都会经过 base64 编码,并且编码前不少于 128
位,每次请求序号都会改变。总之一个原则:不能让攻击者猜到它的内容。
Hashes 是通过在 CSP 中指定允许资源的摘要签名,达到限制非法 inline 代码的目的。签名规则与 Subresource Integrity 一致,支持 sha256、sha384 和 sha512 三种算法。具体使用方式如下:
Content-Security-Policy: script-src
'sha256-d3ii1Pel57UO62xosCMNgTaZJhJa87Gd/X6e7UdlEU8='
<script>alert(0);</script>
Hashes 值可以使用 openssl 工具来计算(以 sha256 为例):
echo -n 'alert(0);' | openssl dgst -sha256 -binary | openssl enc -base64 -A
d3ii1Pel57UO62xosCMNgTaZJhJa87Gd/X6e7UdlEU8=
Node.js 也可以方便地计算 Hashes 值:
var crypto = require('crypto');
function getHashByCode(code) {
var algorithm = 'sha256';
return algorithm + '-' + crypto.createHash(algorithm).update(code, 'utf8').digest("base64");
}
需要注意的是,inline 代码首尾的空白部分也会计算在 hash 之中。
可以看出,这两种方式相比直接使用 unsafe-inline
,在安全方面都有很大的提升。其中 Nonces
方案使用成本更低,Hashes
方案更安全。
由于 Nonces 和 Hashes 属于 CSP2 新增内容,所以当前如果使用这种方案,还是要加上 unsafe-inline
,不然在仅支持
CSP1 的浏览器下会有问题。支持 CSP2 的浏览器发现存在 Nonces 或 Hashes 时,会自动忽略 unsafe-inline
。
另外,CSP2 的 Nonces 和 Hashes 不支持下列两种内联写法:
<span style="color:red;">demo</span>
<span onclick="alert(0);">demo</span>
frame-ancestors
指令用来定义页面是否允许被其他页面嵌套,它和 X-Frame-Options 非常类似:
Content-Security-Policy: frame-ancestors 'none'
Content-Security-Policy: frame-ancestors 'self'
分别等同于:
X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN
二者不同之处在于:浏览器实现 X-Frame-Options
时,一般只考虑了最顶层域名是否匹配;而 frame-ancestors
要求每一层都需要考虑。另外,CSP2 协议规定二者同时存在时,X-Frame-Options
应该被忽略,但我在测试过程中发现,Chrome
45+ 并没有按照标准实现。所以有类似需求时,推荐只使用 frame-ancestors
,不要混用。
CSP2 还有很多其他内容,这里不一一介绍了,有兴趣的同学可以直接查看官方文档,或者留言讨论。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8