深入解析 CSP 中 default-src 的作用与实战配置技巧
又踩坑了,CSP 的 default-src 让我静态资源全挂了
上周上线一个新项目,本地跑得好好的,一部署到测试环境,页面直接白屏。F12 一看,控制台全是红的:Refused to load the script 'xxx' because it violates the following Content Security Policy directive: default-src 'self'。好家伙,CSP(Content Security Policy)又来搞事情了。
其实我们项目早就加了 CSP,但之前一直用的是宽松策略,比如 default-src *,或者干脆没设 default-src,只配了 script-src、style-src 这些。这次为了过安全审计,后端同事统一加了个强策略,结果前端直接瘫痪。
我一开始以为是某个第三方库的 CDN 被 block 了,比如 Google Fonts 或者一些 analytics 脚本。但排查发现,连我们自己打包出来的 main.js 和 style.css 都被拦了!这就奇怪了,这些资源明明和页面同源啊,按理说 'self' 应该放行才对。
折腾了半天,原来是 default-src 的优先级问题
我翻了下 MDN,才发现自己一直有个误区:我以为只要显式写了 script-src,那 default-src 就不会影响脚本加载。但事实是,如果某个指令(比如 script-src)没有被显式定义,它才会 fallback 到 default-src。可一旦你显式写了 script-src,那它就完全独立于 default-src 了。
但问题来了——我们的 CSP 头是这样配的:
Content-Security-Policy: default-src 'self'; script-src 'self' https://jztheme.com;
看起来没问题?但我在测试环境发现,页面里有些内联的 <img> 标签引用了 base64 图片,还有一些 fetch 请求打到了我们自己的 API。这些资源类型(img、connect)并没有在 CSP 中显式声明,所以它们会 fallback 到 default-src 'self'。而 'self' 确实包含同源,所以理论上应该能加载。
但为什么还是报错?继续看错误信息,发现被 block 的不只是脚本,还有字体、图片,甚至 WebSocket 连接。我突然意识到:可能我们漏掉了某些资源类型,而它们 fallback 到 default-src 后,因为某些原因被拒绝了。
后来试了下,在本地把 CSP 改成只保留 default-src 'self',其他都不写,结果页面居然能加载了!但一旦加上 script-src,哪怕内容一样,某些资源又挂了。这说明问题不在 default-src 本身,而在于我们没有为所有用到的资源类型显式配置策略。
核心代码就这几行,但得写全
最后我列出了项目里实际用到的所有资源类型:
- 脚本:来自同源 + 一个第三方 API(https://jztheme.com)
- 样式:同源 + 内联 style(用了 nonce)
- 图片:同源 + data:(base64)
- 字体:同源
- AJAX / fetch:同源 + https://jztheme.com/api
- WebSocket:wss://jztheme.com
于是我把 CSP 补全成这样:
Content-Security-Policy:
default-src 'none';
script-src 'self' https://jztheme.com 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self';
connect-src 'self' https://jztheme.com wss://jztheme.com;
frame-ancestors 'none';
base-uri 'self';
注意这里我把 default-src 设成了 'none',这样更安全——任何未显式声明的资源类型都会被拒绝。虽然严格了点,但避免了遗漏导致的安全漏洞。
不过这里有个小坑:我一开始没加 connect-src,结果 fetch 请求被 block 了。因为 fetch、XMLHttpRequest、WebSocket 都属于 connect-src 管辖范围,而它默认会 fallback 到 default-src。但我们设了 default-src 'none',所以必须显式放开。
另外,如果你用了 Web Worker,还得加 worker-src;用了 <object> 或 <embed>,就得配 object-src。总之,default-src 不是万能兜底,而是“最后防线”,一旦你开始细化策略,就得把所有用到的都列出来。
踩坑提醒:这三点一定注意
第一,别以为 'self' 能 cover 所有同源资源。它确实包含协议、域名、端口相同的资源,但像 blob:、data: 这种 scheme 是不包含的,必须单独加。比如图片用 base64,就得写 img-src data:。
第二,default-src 对 frame-ancestors、report-uri、sandbox 这些指令是无效的,它们必须单独设置。我一开始以为设了 default-src 'none' 就能防 iframe 嵌入,结果发现还得加 frame-ancestors 'none' 才行。
第三,开发时可以用 Content-Security-Policy-Report-Only 头来测试策略,不会真的 block 资源,只在控制台报 warning。等确认没问题再切正式头。这个技巧救了我好几次,不然每次改完都要重新部署验证。
对了,还有一个小问题到现在没彻底解决:我们有个老组件用了 eval(),虽然我知道这是 bad practice,但暂时没法重构。所以 script-src 里不得不加 'unsafe-eval'。虽然加了之后功能正常了,但安全评分肯定扣分。不过考虑到这是内部工具,暂时先这么着吧,后续再抽时间干掉它。
总结一下我的配置思路
现在的做法是:先设 default-src ‘none’,然后按需放开。这样比从宽松策略往回收更安全,也更容易发现遗漏。每加一个第三方服务,就去查它需要哪些 CSP 指令,一一补上。
附上我们最终用的 CSP 头(通过 meta 标签注入,方便前端控制):
<meta http-equiv="Content-Security-Policy" content="
default-src 'none';
script-src 'self' https://jztheme.com 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self';
connect-src 'self' https://jztheme.com wss://jztheme.com;
frame-ancestors 'none';
base-uri 'self';
">
注意:如果用 meta 标签,不能使用 'nonce-xxx' 或 'sha256-...' 这种动态值(因为 meta 是静态的),而且 report-uri 也不支持。所以我们只在开发或简单场景用 meta,生产环境还是走 HTTP 头。
以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。比如有没有自动化工具能扫描页面用到的资源类型并生成 CSP?我试过几个,但要么太重,要么不准,目前还是手动维护。这个技巧的拓展用法还有很多,后续会继续分享这类博客。

暂无评论