彻底搞懂Content Security Policy中的default-src用法与避坑指南

Designer°广红 安全 阅读 1,887
赞 18 收藏
二维码
手机扫码查看
反馈

我为什么非得折腾 CSP 的 default-src?

说真的,一开始我根本不想碰 Content Security Policy(CSP),感觉这玩意儿就是给安全工程师写的,不是给我们这些赶工期的前端用的。直到有一次上线后半小时,页面全白了——用户反馈图片不显示、第三方组件报错一堆,查了半天发现是运维在响应头里加了个 strict CSP 策略,而我们没适配。

彻底搞懂Content Security Policy中的default-src用法与避坑指南

那次之后我就认命了:CSP 这东西躲不过,尤其是 default-src,它就像是整个策略里的“默认继承类”,你不理它,它就给你找事。

最近项目重构,我们打算正式引入 CSP 来防 XSS 和资源注入攻击。核心问题来了:怎么设置 default-src 才既能保证安全,又不至于把正常功能干掉?于是我拉了几个常见方案对比了一波,亲测踩坑,今天来聊聊我的实战感受。

谁更灵活?谁更省事?

目前主流的 default-src 配置方式其实就三种:

  • 直接设成 ‘self’ + 明确列出域名
  • 用 nonce 或 hash 做内联脚本放行
  • 干脆放宽到允许 data: 或 unsafe-inline(别笑,真有人这么干)

下面一个个来看,哪个能让我少改代码,又能过安全审计。

‘self’ 是底线,但不够用

最基础也最常见的写法:

Content-Security-Policy: default-src 'self';

意思很明确:只允许加载同源资源。JS、CSS、图片、字体、iframe 全都只能来自当前域名。听起来挺安全,对吧?

但我告诉你,这个配置一上,页面大概率直接瘫痪。为什么?因为我们用了 CDN 加载静态资源,比如 React 从 unpkg 引入,字体走的是 Google Fonts,还有个统计脚本挂在另一个子域下。

所以实际必须扩展:

Content-Security-Policy: 
  default-src 'self' https://cdn.jztheme.com https://fonts.googleapis.com;

注意这里有个坑:Google Fonts 实际还会请求 woff2 文件,走的是 fonts.gstatic.com,所以你还得加上这个域名到 font-src 或者单独放开 font-src

Content-Security-Policy: 
  default-src 'self' https://cdn.jztheme.com;
  font-src 'self' https://fonts.gstatic.com;
  style-src 'self' https://fonts.googleapis.com;

看到没?default-src 虽然可以被其他指令覆盖,但你一旦用了细粒度指令(如 script-src、img-src),那对应类型就不会继承 default-src 了。这点很多人搞混,导致策略失效或过度限制。

结论:纯 ‘self’ 不现实;配合多域名单独声明还能接受,但维护成本高,每次换 CDN 都要改策略。

用 nonce 放行内联脚本 —— 我比较喜欢这个

现代框架里难免有内联脚本,比如 SSR 渲染时注入初始数据:

<script>
  window.__INITIAL_STATE__ = {"user": null};
</script>

这种代码在 default-src 'self' 下直接被拦,除非你加 unsafe-inline —— 别急着这么做,那等于开了后门。

更好的办法是用 nonce。流程是这样:

  1. 服务端生成一个唯一随机值(nonce)
  2. 把它插入 CSP 头和 script 标签中
  3. 浏览器对比两者是否一致,一致才执行

示例代码:

// Node.js / Express 示例
const nonce = Buffer.from(Date.now().toString()).toString('base64');

res.setHeader(
  'Content-Security-Policy',
  default-src &#039;self&#039;; script-src &#039;self&#039; &#039;nonce-${nonce}&#039;;
);

// 在 HTML 中使用
<script nonce="abc123">
  window.__INITIAL_STATE__ = {"user": null};
</script>

注意:这里的 nonce 值必须每次请求都不同,否则会被缓存绕过,失去意义。我之前图省事用了固定值,结果安全扫描直接标红,提醒“nonce predictable”。

优点很明显:精准控制哪些内联脚本能执行,不怕 XSS 注入伪造脚本。缺点是你要改造模板系统,把 nonce 传进去,前后端得配合好。

我在两个项目里用了这套方案,虽然初期折腾了两天,但现在 CI/CD 自动生成 nonce,基本无感了。如果你做的是中后台或 SSR 应用,我强烈建议走这条路。

hash 方案:适合静态构建,但灵活性差

另一种放行内联脚本的方式是计算其内容的 SHA-256 hash:

<script>
  console.log("hello");
</script>

对应的 hash 是:

script-src 'sha256-qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng='

这种方式的好处是不需要服务端动态生成值,适合纯静态部署(比如 Vite + GitHub Pages)。Webpack 插件甚至可以自动提取并注入 hash。

但我踩过一个大坑:只要脚本里多一个空格、换一行,hash 就变了。某次我加了个 log 调试,忘了删,上线后脚本全被拦截,用户登录不了。排查日志才发现是因为 hash 不匹配。

而且你没法预知所有内联脚本的内容,特别是在动态渲染场景下,hash 基本不可行。

总结:hash 更适合完全静态的项目,且脚本内容固定。对于我们这种常改逻辑的业务系统,太脆弱,我不推荐。

放宽到 data: 或 unsafe-inline?别害人害己

有些团队为了快速上线,直接这么配:

Content-Security-Policy: default-src 'self' data: 'unsafe-inline';

看起来啥都能跑了,没错。但这也意味着任何注入的 base64 图片或内联脚本都能执行。XSS 攻击最爱这种配置。

我们曾接手一个老项目,就是这么配的。后来发现某个富文本编辑器被 XSS 利用,直接弹窗盗 cookie。审计报告直接打了个 F。

所以听我的:永远不要在线上环境开启 ‘unsafe-inline’ 或无限制 data:。开发环境临时用用可以,上线前必须砍掉。

我的选型逻辑

综合下来,我的选择顺序是:

  1. 优先使用 default-src 'self' + 明确列出可信域名
  2. 内联脚本一律用 nonce 控制,杜绝 unsafe-inline
  3. 静态资源尽量走 HTTPS,避免 data: 嵌入过多内容
  4. 必要时拆分指令(如 img-src、connect-src),不要全靠 default-src

举个我们现在的完整策略头:

Content-Security-Policy:
  default-src 'self' https://api.jztheme.com;
  script-src 'self' 'nonce-random123' https://cdn.jztheme.com;
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
  img-src 'self' data: https:;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self' https://api.jztheme.com;
  frame-src 'none';
  object-src 'none';
  base-uri 'self';

说明几点:

  • style-src 里还留着 ‘unsafe-inline’,因为某些 UI 框架用 style 标签写样式,暂时没精力改(有空会换成 nonce)
  • img-src 允许 data: 是因为图标太多小图用 base64,但限制只能是 https 协议外链
  • frame-srcobject-src 设为 ‘none’ 是为了防点击劫持和 Flash 类风险

这个策略不是完美的,但它在安全与可维护性之间找到了平衡点。改完后 QA 测试没发现异常,安全扫描也过了。

以上是我的对比总结,有不同看法欢迎评论区交流

说实话,CSP 这东西没有银弹。你得根据项目类型、部署方式、团队能力来做取舍。我见过用 Webpack 全打包然后靠 hash 跑起来的 SPA,也见过完全靠网关统一下发策略的大厂架构。

但对于大多数中小型项目,我觉得“default-src + nonce”是最务实的选择。虽然前期要改一点代码,但长期看既安全又可控。

最后提醒一句:上线前一定要用 Report-Only 模式先跑几天,收集违规报告,别一上来就 blocking,不然半夜报警你就知道了。

以上是我踩坑后的总结,希望对你有帮助。如果有更优雅的实现方式,或者你觉得 nonce 太麻烦,欢迎留言聊聊。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论