Content Security Policy生成器
生成Content-Security-Policy头部。
Build a Content-Security-Policy. Common keywords: 'self' 'none' 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' data: https: blob:
概述
Content-Security-Policy 告诉浏览器它可以从哪些来源加载资源。它的实际价值比名字听上去要窄一些:CSP 并不修复跨站脚本,它围堵跨站脚本。如果一次注入穿过了你的输出编码,那么一份严格的策略就是阻止被注入的脚本执行、或者阻止它把偷到的东西发往攻击者服务器的那道防线。
这个头部本身是一串用分号分隔的指令,每条指令指明一种资源类型,以及允许这种资源使用的那些来源。这个工具为每条指令提供一个输入框,预先填好一份合理的起始策略,并在你输入的同时同步显示拼装出来的头部。
使用方法
- 从预填的策略开始——
default-src 'self'、object-src 'none'、base-uri 'self'、frame-ancestors 'none'。对于自己托管全部资源的站点来说,这是一个合理的下限。 - 一条指令一条指令地加上你真正需要的那些源。多个来源之间用空格分隔。
- 把某条指令留空就等于省略它;被省略的指令会回退到
default-src。 - 复制头部,先以
Content-Security-Policy-Report-Only部署。读违规报告、修策略,然后再换成强制生效的那个头名。
来源关键字
| 关键字 | 含义 |
|---|---|
'self' | 文档自身的源——同协议、同主机、同端口 |
'none' | 什么都不允许。只有作为唯一来源时才有意义 |
'unsafe-inline' | 允许内联的 <script> / <style> 以及内联事件处理器 |
'unsafe-eval' | 允许 eval、new Function,以及给 setTimeout 传字符串参数 |
'strict-dynamic' | 信任由已受信脚本创建出来的脚本。会让该指令里的主机白名单被忽略 |
data: | 允许 data: URI。用在 img-src 上合理,用在 script-src 上危险 |
https: | 任何走 HTTPS 的源。范围太宽——通常说明这份策略需要收紧 |
blob: | 允许 blob: URL,从生成的代码创建 Web Worker 时需要它 |
哈希('sha256-…')和 nonce('nonce-…')是 'unsafe-inline' 的两种严格替代品。一个哈希覆盖的是某一段内容永远不会变化的内联脚本,这适合静态生成的站点。而 nonce 是每个响应都重新生成一次的随机值,这适合服务端渲染的页面。你不能在静态文件托管上使用 nonce,因为那里根本没有一个按请求执行的环节来生成它。
容易写错的那几条指令
base-uri 不受 default-src 兜底。缺了它,一个被注入的 <base href> 标签就能把页面上每一个相对 URL 都重定向到攻击者的主机——你的 script 标签也在其中——而它完全不需要注入任何脚本本身。把它设成 'self' 或 'none'。
form-action 同样不受 default-src 兜底。缺了它,一个被注入的表单可以把用户的输入 POST 到另一个源去。
frame-ancestors 取代 X-Frame-Options。两者说法冲突时,现代浏览器听 CSP 的,所以只设那个旧头部,会让较新的浏览器完全不受你本意想表达的任何约束。
object-src 'none' 值得显式写上。插件内容是一条遗留下来的执行路径,在一个新站点上没有任何正当用途。
connect-src 是人们最常忘掉的一条。它管辖 fetch、XHR、WebSocket 和 sendBeacon——而这恰恰就是一个被注入的脚本用来把数据外传出去的途径。一个收得很紧的 script-src 配上一个大开着的 connect-src,等于把本来要关上的那扇门原样留在那里。
示例
一个自己托管全部资源、不含任何第三方脚本的静态站点:
default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none';
form-action 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:;
font-src 'self'; connect-src 'self'; upgrade-insecure-requests
同一个站点再加上一个分析标签,只放开它确实需要的那几个源,而不是直接写 https::
script-src 'self' https://www.googletagmanager.com;
connect-src 'self' https://www.google-analytics.com
收集违规报告
Report-Only 模式只有在你真的会去读报告的时候才有用。现有两种机制,浏览器的支持情况是分裂的,所以上线时通常两个都发。
report-uri /csp-report 是较老的那条指令。它已经被废弃,但支持面仍然最广,它会 POST 一个 JSON 体,描述被拦下的资源、拦下它的那条指令,以及文档的 URL。
report-to 是它的替代品。它指向一个由单独的 Reporting-Endpoints 响应头配置的报告分组,这样一个端点就能同时承接 CSP、废弃特性和干预三类报告。
不管你用的是哪一个,都要预期会有噪音。浏览器扩展会往页面里注入脚本和样式,而它们造成的违规在报告里和你自己代码造成的违规长得一模一样。真正值得当成信号的是:在同一个文档 URL、同一条指令下,出现在许多不同用户身上的那种违规;而一条孤零零指向 chrome-extension: 协议的 blocked-uri,那不过是某个人的密码管理器。
这两条指令在 <meta> 标签里都不可用。上报必须依赖真正的 HTTP 响应头。
说明
upgrade-insecure-requests 会在子资源请求发出之前,把 http:// 改写成 https://。它是混合内容页面的一种迁移辅助手段,不能替代把 URL 本身改对,而且它不作用于跳转到其他站点的导航。
style-src 通常是最后一个才去掉 'unsafe-inline' 的地方,而且往往也是值得留着的那一个。内联 style 属性没法用哈希覆盖——你得用 'unsafe-hashes',再为每一个不同的属性值各配一个哈希——而 CSS 注入本身是一个比脚本注入窄得多的问题。先把力气花在 script-src 上,是更划算的取舍。
在切到强制生效之前,先用 Content-Security-Policy-Report-Only 部署。一份只严了一条指令的策略不会优雅地降级:它会对每一个执行它的浏览器背后的访客悄无声息地把页面弄坏,而你会从用户那里、而不是从自己的测试里得知这件事。
如果你想看某个线上站点当前实际发送了什么,HTTP 头信息检查 可以显示任意 URL 的响应头。