如何用 CSP 保护自己的网站

2024-10-08 pv

Web 世界很精彩,同时也很危险。

最近公司一直在推零信任网络 (Zero Trust),尽管这通常是针对内网环境,但在公网环境下,安全同样重要。

根据维基百科中 2010 年的 报告🔗,Web 安全威胁前十名中,XSS (Cross-site scripting,跨站脚本攻击) 排名第二,仅次于代码注入 (Injection)。

当前这个网站,是通过设置 HTTP 头字段中的 Content-Security-Policy<meta> 字段减少 XSS。

1. CSP 含义

XSS 攻击,利用了浏览器对于服务器所获取内容的信任

如果获取到的是恶意脚本,在本地执行就会很危险,因为浏览器误认为这是一段正常程序。

CSP 就是通过限制可执行脚本的来源,从而减少 XSS 攻击的可能性。相当于指定一份白名单,即便恶意代码被注入,因为来源不在白名单中,会被拒绝执行。

CSP 的全称是 Content-Security-Policy,即:内容安全策略

使用的方式有两种。

一种是配置服务器,返回 Content-Security-Policy🔗 HTTP 标头。

另一种是配置 <meta> 字段,类似:

<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; img-src https://*; child-src 'none';"
/>

2. CSP 使用

CSP 的语法遵循下列格式:

Content-Security-Policy: <policy-directive>; <policy-directive>

具体的 policy 可通过 指令 选项值 构建。

指令主要有 5 种。获取指令,文档指令,导航指令,报告指令和其他指令等。具体可参照:Content-Security-Policy - HTTP | MDN🔗

常用的有以下这些:

  • child-src:为 Web Workers 和其他内嵌内容,例如 <iframe> 加载的内容定义合法源
  • connect-src:指定可以通过脚本加载的 URL
  • default-src:为其他指令提供备用服务,可理解为兜底
  • font-src:设置允许 @font-face 加载的字体源
  • frame-src:指定允许 <iframe> 加载的源
  • img-src:限制加载图片的源
  • manifest-src:限制应用声明文件的源
  • media-src:限制通过 <audio><video><track> 标签加载的媒体文件的源
  • object-src:限制 <object><embed> 标签的源
  • prefetch-src:指定预加载允许的源
  • script-src:限制 JavaScript 加载的源
  • style-src:限制 CSS 文件加载的源
  • worker-src:限制 Worker、SharedWorker 或 ServiceWorker 脚本源
  • block-all-mixed-content:当使用 HTTPS 时阻止使用 HTTP 加载任何资源
  • upgrade-insecure-requests:让浏览器把所有 HTTP 访问替换为 HTTPS 访问

选项值通常是由下列格式构成:

<协议>://<主机>/<路径>

可以使用通配符 * 进行模糊匹配,选项值可以有多个。

另外单独有两个关键词,'self''none',分别表示当前域名和禁止加载外部资源,各自都需要加引号。

例如:

Content-Security-Policy: default-src 'self' *.mailsite.com; img-src *

单独针对 script-src,还有 5 个特殊值。

  • unsafe-inline:允许内联脚本和样式,即下列形式:

    <script>
    const inline = 1;
    // …
    </script>

    但是,允许内联脚本执行,通常被认为有安全风险。因此建议使用 nonce 值 和 hash 值代替。

    • nonce 值:使用加密安全的随机令牌生成器生成一个随机 nonce 值,并包含在策略中,因为是动态生成的,因此每次的 HTTP 请求返回结果都不同

      Content-Security-Policy: script-src 'nonce-2726c7f26c'

      同时将生成的 nonce 值放置在内联脚本中。

      <script nonce="2726c7f26c">
      const inline = 1;
      // …
      </script>
    • hash 值:也可根据内联脚本生成 hash 值,CSP 支持 sha256, sha384 和 sha512

      Content-Security-Policy: script-src 'sha256-B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8='

      sha256 的值生成自脚本(不包含 script):

      <script>
      const inline = 1;
      </script>
  • unsafe-hashes:按照上述办法,event handler 依然不允许执行,unsafe-hashes 可用于解决此等困境

  • unsafe-eval:允许将字符串当作脚本执行,例如 eval(), Function() 等

  • wasm-unsafe-eval:如果没有该字段,WebAssembly 将被拒绝执行

  • strict-dynamic:对于通过 nonce 值或 hash 值完成信任的脚本,其所加载的脚本也被信任

另外,CSP 中 report-uri 指令已经废弃,类似功能可通过 Content-Security-Policy-Report-Only 响应头实现。

3. 最佳实践

为网站部署 CSP,其实就像零信任网络一样,不应该信任任何外部未经验证的对象。

3.1 最低权限

CSP 的核心是尽可能使用白名单,从严格到宽松,尽量不要使用 *,因为会降低 CSP 的效果。

通常使用 default-src 指令作为默认的回退策略,

例如:

Content-Security-Policy: default-src 'self'; script-src 'self' https://example.com; style-src 'self';

3.2 访问控制

避免使用内联脚本和样式,因为攻击者可以插入恶意代码。如果一定要用,可以使用上文中提到的 nonce 值或 hash 值。

避免使用类似 eval() 等不安全的 JavaScript 方法。

对于图片、字体、媒体等,最好使用相应指令明确信任源。

防止 HTTPS 页面加载 HTTP 资源,从而导致降级攻击,可以用 upgrade-insecure-requests 指令自动升级。

3.3 持续监控

部署时可以先紧后松,从最严格集合开始,根据实际效果不断放松。

另外需注意,CSP 应伴随网站内容结构改变而动态调整,因此定期的校验会是个不错的办法。

4. 总结

在文章的开始,就把零信任网络拿出来讲,是因为在我看来,CSP 的理念,和零信任网络异曲同工。

假设外部环境存在威胁,必要时授予最低权限,时刻观察行为和后果。

在安全(Security)和灵活(Flexible)中找到平衡。

悲观地去假设,采取一定的策略规避最坏情况的发生,然后乐观地生活。

我想,这可能就是最近经常谈及的底线思维吧。

(完)

参考

  1. Content-Security-Policy - HTTP | MDN🔗
  2. 零信任安全 | 什么是零信任网络? | Cloudflare🔗
  3. Content Security Policy 入门教程 - 阮一峰的网络日志🔗
在 GitHub 上编辑本页面

最后更新于: 2024-10-10T05:43:55+08:00