CSP:给大家介绍下我爸爸 — Performance

773 阅读7分钟
原文链接: zhuanlan.zhihu.com

这篇文章主要讲如何利用 PerformanceAPI 辅助书写 CSP 指令 里的域名名单的思路,并不是写 CSP 的最佳实践,也未提供成熟工具。如果你还不了解 CSP 或者 PerformanceAPI,可以看下第 0 步(由于我并未特别深入 CSP 规则,如果有写的不对的地方,请大力指正),大佬请跳过。

0. 名词解释

可以理解为资源加载 (CSS, img, JS, connect, iframe, media...) 的域名白名单,防止应用加载到有危险的脚本之类的,可以通过 meta 标签书写 or 直接在 Response Header 里写入

记录当前页面中与性能相关的信息,打开浏览器 Devtool 里 Network 面板随意查看一个资源的请求的 Timing 其实就可以在 Perf 中找到相关联的数据,例如某字体的下载时间:

Perf 广泛用在前端监控中,这里不多讲。今天只说其中的 PerformanceResourceTiming ,一个记录了特定资源的下载信息的对象。

1. CSP 与 Perf : 需求与供应

先来看一段 Github 的 CSP 内容:

Content-Security-Policy:
    - default-src 'none';
    - base-uri 'self'; 
    - block-all-mixed-content; 
    - connect-src 'self' uploads.github.com status.github.com collector.githubapp.com api.github.com www.google-analytics.com github-cloud.s3.amazonaws.com github-production-repository-file-5c1aeb.s3.amazonaws.com github-production-upload-manifest-file-7fdce7.s3.amazonaws.com github-production-user-asset-6210df.s3.amazonaws.com wss://live.github.com; 
    - font-src assets-cdn.github.com;
    - form-action 'self' github.com gist.github.com;
    - frame-ancestors 'none'; 
    - frame-src render.githubusercontent.com; 
    - img-src 'self' data: assets-cdn.github.com identicons.github.com collector.githubapp.com github-cloud.s3.amazonaws.com *.githubusercontent.com; 
    - manifest-src 'self'; 
    - media-src 'none';
    - script-src assets-cdn.github.com; 
    - style-src 'unsafe-inline' assets-cdn.github.com

可以看到是非常详细的,尤其是 connect-src 那长长长长长长长长的一串。那么问题来了,如果让你你在项目中去编写这段规则,你会怎么做呢:

首先想到的肯定是手动穷举,但在中小项目且你清晰全链路的情况下这样做没什么问题,甚至 Policy 全部降级到 default-src 问题也不大。但穷举缺点也很明显:

1. 项目一大或者是接手的项目就很浪费时间

2. Policy 拆分可能得看你的心情

3. 懒怎么办 。

说白了除非有具体硬指标,否则 CSP 也只是一个锦上添花的东西,大部分开发日常可能都不会重视,不重视怎么可能会花心思去做好呢,那么有没有这么一款工具或者一段代码能够直接 根据项目 生成足够可用的 CSP 指令呢!!!

并没有。

不过不过不过轮到主角登场了,我们可以借助 Perf 来 得知项目会获取哪些资源,试着在控制台运行:

function assetParser(url) {
  const { host, pathname } =  new URL(url)

  const assetName = pathname.split('/').pop()

  return { assetType: assetName.includes('.') ? assetName.split('.').pop() : null, name: assetName || host, host}
}

performance
  .getEntriesByType('resource')
  .map(({ name, initiatorType }) => {
      return {
          initiatorType,
          ...assetParser(name)
      }
  })

你会看到(下图为Chrome Canary 68 版本的输出,为什么要强调浏览器版本呢,大概你也猜到了,后面会有说明):

我们单独拎出一个来粗略地说明下

  • assetType: 资源类型

这个字段并非 Perf 提供,而是我们根据请求的 url 切割出来的,如果该字段:

- 有具体值 —— 那么在大部分情况下足以说明该资源的类型,常见为一些图片格式(png / jpg / svg...)、JS、CSS【诶!是不是感觉可以和 CSP 里的 img-src / script-src / style-src / font-src ... 对应上了呢?】

- 为 null —— 这种情况下说明路径没有 extension 或者不是个普通的静态资源,这时需要我们再来看的另一个字段:initiatorType

这是 Perf 内置的属性,表明了该资源的发起者(某个 API or 某 DOM.localName)是什么类型。

例如我们看到值是 link,那么说明该资源是通过 link 标签加载的(直接加载或者 preload / prefetch)其余的值例如:

- 如果是 iframe 、video、audio、img 则说明是通过对应标签加载的【咦!是不是也对应 CSP 内 frame-src 和 media-src、img-src了】

- 如果是 fetch、xhrhttprequest、beacon 则说明是请求【对!可以对应 connect-src 】

But,但在更多情况下,这个字段是有迷惑性的,因为有时它并不直接代表资源类型

例如一张图片资源类型是一张图片,为什么 initiatorType 会是 css 呢?因为根据定义,这张图片很可能是 CSS 里的 background-image 通过 url 方法加载的,所以它的发起者的类型是 css ,那么这就会引出一个问题

如果这种背景图片不是以显示的资源路径( https://host/path/avatar.jpg ) 的形式加载,而是以( https://host/path/avatar ) 这种接口进行加载,那么这是 assetType 的判断肯定是无效,而 initiatorType 又是 css,这时候工具(我们假设有个自动生成 CSP 的工具,这也是之后最终的目的)该把这个 host 写在 CSP 的 img-src 下呢还是 style-src 下呢?

目前的想法是这一类的资源都应该降级为 default-src,或者交给人工处理(这不废话吗

写到这里,CSP 的需求 — Perf 的供应关系貌似已经明朗。配合 assetType / initiatorType / host 三个字段配合,再写几行代码,目前看来足以生成囊括 img-src / script-src / style-src / font-src / frame-src / media-src / connect-src 以及 default-src 的 CSP 指令了。

但是!又来但是了,事情真的这么顺溜儿吗,前面提到截图是来自 Chorme Canary 68 版本,一个很新很新的版本,为什么要用这个版本呢,因为到目前为止 (2018-08-02) ,Chrome 对 initiatorType 判断还存在着 Bug ,这是个2016年被提出的 Bug,到现在还未在正式版完整的解决,这里提到有一段:

老版本内核发起的 fetch 、 video 等的 initiatorType 可能为空字符串!!具体涉及哪些版本这里并未深究。因为存在这一个 Bug 对于 connect-src 的控制类似乎走不通了?答案是:

对的,走不通

或许你会说可以用 Canary 在自己本地跑项目然后收集 Pref 信息不就行了?

对的,但这也正是要抛出的一个问题 —— 我们在本地跑项目获取到的 Perf 信息是有局限性的,可能但不限于会有:

  1. 开发环境和生产环境的差异问题
  2. 对于复杂项目本地很难跑遍所有流程,触发所有资源请求

所以通过本地的 Perf 方式想监控到 fetch 的资源也不完美

2. 那蹊径有吗蹊径

PerformanceAPI 主要应用于 —— 前端监控服务

那么,如果我们将这些资源数据上报到监控服务,在一定量级后,我们捞出的聚合数据就可以解决上面说的环境差异和覆盖率问题,对于除 fetch 外的其他资源的 CSP 规则生成都是没问题了。为什么这里 fetch 还是被排除了?

因为你没办法保证用户的客户端没有 initarortype 的问题,所以对于 connect-src ,再有监控服务的情况下,最好的办法还是进行 fetch 和 xhr 拦截上报,然后捞出数据获取 host。

写着写着感觉文章有种烂尾的感觉了,因为好像没有提供一个清晰的结局方案,那么我们试着来总结下:

3. 最终看起来可行的方案

  • 静态资源通过 performance.getEntriesBytype('resource') 获取信息进行上报,然后可以对应 img-src / script-src / style-src / font-src / frame-src / media-src ... 基本上除了 connect-src之外的绝大部分(也是够用的) CSP 字段
  • 动态资源(接口请求等) 通过请求拦截器进行上报,用来拼接 connect-src
  • 对于那些无法判断类型的资源则用 default-src 进行降级

最终当然要提供一个可视化工具,允许用户选择需要的指令:

4. 参考链接

再不要脸来个广告: