Content Security Policy (CSP) 介绍
当我不经意间在 Twitter 页面 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Twitter</title>
<style>
body {
background-color: #ffffff;
font-family: sans-serif;
}
a {
color: #1da1f2;
}
svg {
color: #1da1f2;
display: block;
fill: currentcolor;
height: 21px;
margin: 13px auto;
width: 24px;
}
</style>
</head>
<body>
<noscript>
<center>If you’re not redirected soon, please <a href="/">use this link</a>.</center>
</noscript>
<script nonce="SG0bV9rOanQfzG0ccU8WQw==">
document.cookie = "app_shell_visited=1;path=/;max-age=5";
location.replace(location.href.split("#")[0]);
</script>
</body>
</html>
相比平时看到的其他站点的源码,可以说是很清爽了。没有乱七八糟的标签,功能却一样不少。特别有迷惑性,以为这便是页面所有的源码,但查看 DevTools 的 Source 面板后很容易知道这并不是真实的 HTML 代码。但为何页面源码给出的是如此清爽的版本,这里先不研究。 把目光移向 script 标签时,发现一个不认识的 ! <script nonce="SG0bV9rOanQfzG0ccU8WQw==">
document.cookie = "app_shell_visited=1;path=/;max-age=5";
location.replace(location.href.split("#")[0]);
</script>
Content Security Policy (CSP)要了解 我们都知道浏览器有同源策略(same-origin policy)的安全限制,即每个站点只允许加载来自和自身同域(origin)的数据,
现实中,问题是同源策略也并不是万无一失,跨域攻击 Cross-site scripting (XSS) 便包含五花八门绕开限制的手段,形式上通过向页面注入恶意代码完成信息的窃取或攻击。比如 UGC 类型的站点,因为内容依赖用户创建,这就开了很大一个口子,允许用户输入的内容运行在页面上。当然,因为我们都知道会有注入攻击,所以对用户输入的内容进行防 XSS 过滤也成了标配。 Content-Security-Policy 从另一方面给浏览器加了层防护,能极大地减少这种攻击的发生。 原理CSP 通过告诉浏览器一系列规则,严格规定页面中哪些资源允许有哪些来源, 不在指定范围内的统统拒绝。相比同源策略,CSP 可以说是很严格了。 其实施有两种途径:
 为了测试方便,以下示例均使用 一个简单示例创建一个 HTML 文件放入以下内容: csp_test.html <!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' https://unpkg.com">
<title>CSP Test</title>
</head>
<body>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
</body>
</html>
在该测试文件所在目录开启一个本地 server 以访问,这里使用 Python 自带的 server: $ python -m SimpleHTTPServer 8000
然后访问 localhost:8000 以观察结果: 然后我们将 csp_test.html <!DOCTYPE html>
<html lang="en">
<head>
- <meta http-equiv="Content-Security-Policy" content="script-src 'self' https://unpkg.com">
+ <meta http-equiv="Content-Security-Policy" content="script-src ‘none’>
<title>CSP Test</title>
</head>
<body>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
</body>
</html>
下面我们来解释这里设置的 CSP 规则及理解为何资源加载失败。 CSP 规则无论是 header 中还是 示例:
这里 directive,即指令,是 CSP 规范中规定用以详细详述某种资源的来源,比如前面示例中使用的
默认情况下,这些指令都是最大条件开放的,可以理解为其默认值为 还有种特殊的指令 常见的做法会设置
现在来看开头那个示例,也许现在就能看明白了。因为页面中需要从 CDN 加载 React 库,所以我们
这里的 改成 test.js
同时在页面中引用它: csp_test.html <!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Security-Policy" content="script-src 'none'">
<title>CSP Test</title>
</head>
<body>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
+ <script src="./test.js"></script>
</body>
</html>
页面执行结果: 是的,哪怕是自己的脚本也无法被加载执行。CSP 就是这样严格和明确,不存在模棱两可的情况。所以在指定来源时,我们需要确认 URI 是否正确。 指令可接受的值指令后面跟的来源,有两种写法
预设值其中预设值有以下这些:
特别地,在 CSP 的严格控制下,页面中内联脚本及样式也会受影响,在没有明确指定的情况下,其不能被浏览器执行。 考虑下面的代码: csp_test.html <!DOCTYPE html>
<html lang="en">
<head>
<title>CSP Test</title>
<style>
body{
color:red;
}
</style>
</head>
<body>
<h1>Hello, World!</h1>
<script>
window.onload=function(){
alert('hi jack!')
}
</script>
</body>
</html>
根据 MDN 上的描述,如果站点未指定 CSP 无则,浏览器默认不会开启相应检查,所以上面一切运行正常,只受正常的同域限制 。
我们加上 CSP 限制: csp_test.html <!DOCTYPE html>
<html lang="en">
<head>
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'">
<title>CSP Test</title>
<style>
body{
color:red;
}
</style>
</head>
<body>
<h1>Hello, World!</h1>
<script>
window.onload=function(){
alert('hi jack!')
}
</script>
</body>
</html>
配置站点默认只信息同域的资源,但注意,这个设置并不包含内联的情况,所以结果会如下图。 如何修复它呢。如果我们想要允许页面内的内联脚本或样式,则需要明确地通过 csp_test.html <!DOCTYPE html>
<html lang="en">
<head>
! <meta http-equiv="Content-Security-Policy" content="default-src 'self' ‘unsave-inline’”>
<title>CSP Test</title>
<style>
body{
color:red;
}
</style>
</head>
<body>
<h1>Hello, World!</h1>
<script>
window.onload=function(){
alert('hi jack!')
}
</script>
</body>
</html>
这里 刷新页面,样式及脚本又可以正常执行了。 通常是不建议使用 如果页面中非得用内联的写法,还有种方式。即页面中这些内联的脚本或样式标签,赋值一个加密串,这个加密串由服务器生成,同时这个加密串被添加到页面的响应头里面。 <script nonce=EDNnf03nceIOfn39fn3e9h3sdfa>
// 这里放置内联在 HTML 中的代码
</script>
页面 HTTP 响应头的
注意这里的 这也就是文章开头看到的方式,到这里明白了。
这里的加密串一定是随机不可预测的,否则达不到安全效果,且每次页面被访问时重新生成。 除了使用 hash 方式的示例: <script>alert('Hello, world.');</script>
evaljs 中好些地方是可以以字符串方式动态创建代码并执行,这被认为是不安全的,所以不推荐使用,一般最佳实践里都会提。
和内联一样,有专门的指令 setTimout(function(){
alert(1);
}, 1000)
URI除了上面的预设值,还可通过提供完整的 URI 或带通配符
因为 URI 是进行动态匹配的,所以解释了上面提到的预设值缘何要加引号。因为如果不加引号的话, 优先级CSP 的配置是很灵活的。每条指令可指定多个来源,空格分开。而一条 CSP 规则可由多条指令组成,指令间用分号隔开。各指令间没有顺序的要求,因为每条指令都是各司其职。甚至一次响应中, 我们来看这些情形下 CSP 的表现。
csp_test.html <!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'self';default-src 'unsafe-inline';">
<title>CSP Test</title>
<style>
body{
color:red;
}
</style>
</head>
<body>
<h1>Hello, World!</h1>
<script>
window.onload=function(){
alert('hi jack!')
}
</script>
</body>
</html>
很智能地, 浏览器不仅会将检测不过的资源及指令打印出来,重复配置时被忽略的指令也会提示出来。
发送报告当检测到非法资源时,除了控制台看到的报错信息,也可以让浏览器将日志发送到服务器以供后续分析使用。接收报告的地址可在
服务端拿到的是以 JSON 形式传来的数据。 {
"csp-report": {
"document-uri": "http://example.org/page.html",
"referrer": "http://evil.example.com/",
"blocked-uri": "http://evil.example.com/evil.js",
"violated-directive": "script-src 'self' https://apis.google.com",
"original-policy": "script-src 'self' https://apis.google.com; report-uri http://example.org/my_amazing_csp_report_parser"
}
}
报告模式CSP 提供了一种报告模式,该模式下资源不会真的被限制加载,只会对检测到的问题进行上报 ,以 JSON 数据的形式发送到 通过指定
当然,你也可以同时指定两种响应头,各自里的规则还会正常执行,不会互相影响。比如:
这里图片还是会正常加载,但是 报告模式对于测试非常有用。在开启 CSP 之前肯定需要对整站做全面的测试,将发现的问题及时修复后再真正开启,比如上面提到的对内联代码的改造。 推荐的做法这样的安全措施当然是能尽快启用就尽快。以下是推荐的做法:
浏览器兼容性目前发布的 Level 3 规范 中大部分还未被浏览器实现,通过 Can I Use 的数据 来看,除 IE 外,Level 2 的功能已经得到了很好的支持。这里还有一分来自 W3C 跟踪的各浏览器实现情况的统计:Implementation Report for Content Security Policy Level 2。 对于浏览器不支持的情况,也不必担心,会回退到同源策略的限制上。 相关资源 |