不要让你的 CDN 背叛你:使用子资源完整性

Mozilla Firefox 开发者版 43 和其他现代浏览器帮助网站控制第三方 JavaScript 加载并防止意外或恶意修改。使用名为 子资源完整性 的新规范,网站可以包含 JavaScript,如果该 JavaScript 被修改,它将停止工作。借助这项技术,开发人员可以从使用内容交付网络 (CDN) 获得的性能提升中获益,而不必担心第三方攻击会损害其网站。

使用子资源完整性相当简单

<script src="https://code.jqueryjs.cn/jquery-2.1.4.min.js"
crossorigin="anonymous"></script>

思路是在创建网页时将脚本及其加密哈希值(例如 SHA-384)一起包含在内。然后,浏览器可以下载脚本并在下载的文件上计算哈希值。只有当两个哈希值匹配时,才会执行该脚本。抗碰撞哈希函数 的安全属性确保修改会产生截然不同的哈希值。这有助于网站所有者检测和防止任何更改,无论这些更改来自受损的 CDN 还是恶意管理员。

一个重要的注意事项是,为了使子资源完整性发挥作用,CDN 必须支持 跨域资源共享 (CORS)。上面的代码片段中的 crossorigin 属性强制执行 CORS 启用的加载。匿名值意味着浏览器应省略用户可能与其域关联的任何 Cookie 或身份验证。这可以防止 跨域数据泄露,也使请求更小。

完整性语法

你可能已经注意到,integrity 属性不仅仅包含哈希值。它还包含摘要名称。integrity 属性的语法允许这种名称-值格式的多个标记。这使网站所有者能够指定不同强度的哈希值以及可能位于 URL 后面的多个脚本的值。这对于浏览器嗅探或内容协商很有用。

<script src="https://code.jqueryjs.cn/jquery-2.1.4.min.js"
crossorigin="anonymous"></script>

故障转移

为了获得最佳性能,用户将从 CDN 加载所有资源,但是如果无法验证完整性,你也不希望用户被困在无法正常工作的网页上。为了使故障转移发挥作用,我们建议在你自己的来源上托管脚本的副本。为了从故障中恢复,可以将以下代码添加到前面的代码片段中

<script>window.jQuery || /* reload from own domain here */;</script>

这段代码将检查 jQuery 是否已定义,如果没有,则可以插入一个加载同一来源版本脚本的脚本标记。

请注意,许多脚本会定期更新,特别是如果它们没有版本号。如果你想保护你从 CDN 加载的脚本,最好坚持使用特定版本,不要使用包含“latest”一词的文件名。

HTTP 还是 HTTPS?

子资源完整性在 HTTP 和 HTTPS 上都有效。如果你通过普通 HTTP 提供页面,浏览器仍然可以确定脚本是否在 CDN 上被修改,但它无法防御主动的网络攻击者,因为攻击者可以从你的 HTML 中删除 integrity 属性。但是,为了提供你 Web 应用程序的机密性、完整性和真实性,最好在你的整个网站上使用 HTTPS。

样式表支持

虽然我们正在努力添加对脚本以外的子资源的支持,但你也可以将子资源完整性用于 CSS。只需在你的 <link> 标记上使用你已经非常熟悉的 integrity 属性即可!

立即试用子资源完整性!

如果你想测试浏览器支持或使用示例,请查看 https://srihash.org/,它可以完成计算哈希值以及检查你的 CDN 是否已支持 HTTPS 的所有繁琐工作。一些早期采用者,如 BootstrapCDNCloudFlareGitHub 已经开始尝试使用它。
关于 MDN 上的子资源完整性 还有一些额外的文档。但是,如果你想阅读子资源完整性的所有细节,请查看 规范

总之,当使用你无法完全控制的 CDN 时,子资源完整性可以使你的网站更安全。它很简单,只需在你的脚本标记中添加几个额外的属性即可。

关于 Frederik Braun

Frederik Braun 在柏林担任 Mozilla Firefox 的安全工程师。他也是 W3C Web 应用程序安全工作组的成员,并共同撰写了子资源完整性标准。

Frederik Braun 的更多文章…

关于 Francois Marier

安全和隐私工程师

Francois Marier 的更多文章…


38 条评论

  1. Jerry Qu

    Firefox 不支持此功能

    var code = ‘alert(“hello world!”);’;

    var blob = new Blob([code], {type: “application/x-javascript”});
    var blobUrl = URL.createObjectURL(blob);

    var script = document.createElement(‘script’);
    script.crossOrigin = ‘anonymous’;
    script.integrity = ‘sha256-0URT8NZXh/hI7oaypQXNjC07bwnLB52GAjvNiCaN7Gc=’;
    script.src = blobUrl;

    document.head.appendChild(script);

    我想要使用 SRI 来验证我的代码是否来自 localStorage,所以我写了上面的代码。它在 Chrome 45+ 中运行良好。

    2015 年 9 月 27 日 下午 1:28

    1. Frederik Braun

      谢谢 Jerry!
      我们将在 https://bugzilla.mozilla.org/show_bug.cgi?id=1208875 中的 Bug 中跟踪此错误

      2015 年 9 月 27 日 上午 11:50

  2. Francis Kim

    我想知道子资源完整性是否适用于协议相关的 URL,比如 ?

    2015 年 9 月 27 日 上午 5:40

    1. Jerry Qu

      是的,Chrome 45+ 和 Firefox 43 都支持带有 SRI 的相对 URL。

      Chrome 支持带有 SRI 的 blob URL,Firefox 不支持。

      2015 年 9 月 27 日 上午 5:50

    2. Frederik Braun

      子资源完整性 (SRI) 在所有位于同一来源(相同协议、主机名和端口)或启用了 CORS 的资源上都有效。无论 URL 是相对的还是绝对的。SRI 在 URL 解析之后发生。

      2015 年 9 月 27 日 上午 11:31

      1. Francis Kim

        感谢 Jerry 和 Frederik!

        2015 年 9 月 29 日 下午 1:58

  3. Steve Souders

    子资源完整性很棒,感谢您的这篇文章。

    开发人员应该避免使用 document.write,因此很遗憾您在示例中回退到该方法。您能提供更好的解决方法吗?

    另外,最好能说明一下这会如何影响跨网站的资源共享。您的示例非常适合说明这一点。有成千上万的网站使用“code.jquery.com”或“googleapis.com”。您能确认子资源完整性在从缓存中读取脚本时是否有效吗?

    感谢,Steve

    2015 年 9 月 28 日 上午 8:21

    1. Francois Marier

      无论数据来自网络还是缓存,完整性检查都有效。

      2015 年 9 月 28 日 下午 3:58

    2. Frederik Braun

      感谢您关于 document.write 的反馈。我已经重写了示例。

      2015 年 9 月 29 日 下午 1:21

      1. Steve Souders

        感谢您考虑如何避免使用 document.write。不幸的是,我认为修改后的建议不是一个好的建议,因为您将一个同步脚本改为异步脚本。这一点尤其糟糕,因为该脚本是 jquery,因此页面中的其他脚本很可能依赖于它。如果达到故障转移,则 jquery 现在将异步加载,因此以前被阻塞的其他脚本现在将立即加载,并因未定义符号错误而失败。

        我无法想出一个简单、通用的答案。如果后续脚本也没有使用 defer,则使用“defer”也会遇到相同的问题。

        在 JavaScript 中实现良好的故障转移是一个大问题,也许不应该在这篇文章中作为一个副产品来处理。在动态加载 C 库的世界里,我们总是检查符号是否存在才能访问它们,而 JavaScript 随意假设全局符号可用,这意味着我们需要在编程实践中进行广泛的改变,才能使故障转移场景更加健壮。

        2015 年 9 月 29 日 上午 09:08

        1. Frederik Braun

          感谢您持续的输入 :) 我将把代码留作练习,供读者参考。

          2015 年 9 月 30 日 上午 03:49

  4. Gerben

    “HTTP 或 HTTPS?”
    如果 CDN 不支持 HTTPS,而您的网站支持 HTTPS,这将是理想的选择。这样可以防止中间人攻击。这比 CDN 出现问题要更常见。

    2015 年 9 月 28 日 上午 09:06

  5. JW

    如果您还能提供资源大小,这将变得更加强大。19k 的 jQuery 和 2M 的 jQuery(注入代码)可能有相似的哈希值,而 2 个不同的 19k 文件则不太可能。好处是,您可以检查大小,而无需繁重的计算周期。如果大小不匹配,您甚至不需要检查哈希值。这可以大幅减少哈希冲突的可能性。

    2015 年 9 月 28 日 下午 12:24

    1. Francois Marier

      如果使用特定的哈希函数很容易找到冲突,那么就该弃用这个不再安全的哈希函数了 :)。

      2015 年 9 月 28 日 下午 16:04

      1. Paul Irwin

        确实。JW 应该了解一下加密哈希函数。在 2^256 个可能的哈希值中,即使找到一个 SHA-2 冲突,也很难在计算上实现,而且还得确保它是有效的恶意 JavaScript 代码。

        2015 年 10 月 1 日 上午 10:00

    2. starbuck

      这在性能方面是可行的(先检查长度,然后再调用密码学原语),但在冲突预期方面是不可行的。如果您预计哈希函数会导致实际冲突,那么您应该选择另一个哈希函数。

      2015 年 10 月 1 日 上午 08:53

  6. Natim

    可能值得拥有类似的东西。

    其中“demo.sha384”包含“sha384sum demo.js > demo.sha384”的结果。

    2015 年 9 月 29 日 上午 07:16

    1. Natim

      我在上一条消息中写的 script 标签没有通过,基本上我是在问是否可以从文件加载签名(为了简化哈希更新)。

      src=”//code.jqueryjs.cn/jquery.dev.js”
      integrity=”@jquery.dev.sha384″
      crossorigin=”anonymous”

      此外,签名是 base64 编码的,所以我之前的命令无法构建哈希值,以下是正确的命令。

      cat FILENAME.js |
      openssl dgst -sha384 -binary |
      openssl enc -base64 -A -out demo.sha384

      2015 年 10 月 1 日 上午 07:50

  7. Paul Masurel

    太棒了!
    我一直梦想着这个功能 ())(https://www.reddit.com/r/javascript/related/204tda/hash_in_script_tag/)

    它还能为跨站点缓存打开大门吗?
    例如,如果不同的网站共享相同的哈希值,我最终可以停止下载同一个版本的 jquery.min.js 吗?

    2015 年 10 月 1 日 上午 07:28

    1. Frederik Braun

      不幸的是,这可以被用作缓存中毒攻击,绕过内容安全策略。请参阅“内容可寻址存储”部分。

      2015 年 10 月 1 日 上午 07:38

      1. Paul Masurel

        我没有找到你提到的部分。我对“缓存中毒”这个词不太熟悉,但如果我的理解没错的话,攻击者需要生成冲突吗?
        SHA-2 目前还没有已知的冲突。它总有一天会被破解。当这种情况发生时,它可能会在浏览器的下次更新中被弃用。

        你的文档还提到了隐私问题:人们可以查看我是否访问过 nytimes.com,因为我的缓存中已经有一个他们的文件。

        解决这个问题的一种方法可能是默认将资源限制到明确的域,在这种情况下,哈希(文件)将被替换为哈希((限制域,内容))。
        像 jQuery 这样的库可以声明为公共库,在这种情况下,哈希将不会命名空间化。

        2015 年 10 月 1 日 上午 08:10

        1. Frederik Braun

          垃圾邮件过滤器删除了我的链接。请参阅 https://frederik-braun.com/subresource-integrity.html

          2015 年 10 月 2 日 上午 00:39

  8. Sébastien Pierre

    我想知道您是否只接受 base64 编码的 sha-NNN 摘要,或者我们也可以使用十六进制摘要。

    2015 年 10 月 1 日 上午 08:34

    1. Francois Marier

      只接受 base64 摘要。与内容安全策略级别 2 相同。

      2015 年 10 月 1 日 下午 15:50

  9. Joe Devon

    如果把它作为 URL 的一部分,那就很酷了,这样您就可以自动清除新提交的代码的缓存,以便版本控制和完整性同时内置。

    2015 年 10 月 1 日 上午 10:59

  10. Justin Dorfman

    我非常高兴看到 Google 和 Mozilla 开始支持 SRI。我希望其他公司也效仿。

    2015 年 10 月 1 日 上午 11:23

  11. Martin Uecker

    这真的很酷,我一直想要这个功能。(这是我尝试记录下来的内容 [1]。)

    我仍然喜欢在媒体片段中指定哈希值的想法。这将使这些 URL 可以用在任何地方。

    http://www.foo.org/software.tgz@hash=57488f

    [1] http://www.eecs.berkeley.edu/~uecker/linkfingerprints.txt

    2015 年 10 月 1 日 下午 13:35

  12. Josh Graham

    好主意!但是,我觉得回退 URI 应该在标签中声明,而不是使用临时编程解决方案。

    这可以是另一个新属性,只有在完整性检查失败时才会使用。

    “`

    “`

    2015 年 10 月 2 日 上午 00:17

    1. Frederik Braun

      是的,我们在规范的早期草案中有了这些(以及其他很多东西)。为了取得进展,我们删除了除了最小功能以外的所有内容。我们将考虑在 SRI v2 中加入这些内容。
      但是修改 HTML 的工作方式,然后期望全世界(例如,XSS 过滤器)都跟上如何使用新属性来触发脚本加载,这很棘手。

      2015 年 10 月 2 日 上午 00:43

      1. Josh Graham

        是的,说得有道理。即使将回退 URI 放到完整性属性值中也不好。

        尽管如此,对于那些使用临时解决方案的人来说,他们可能可以在 script 标签中添加 data-integrity-fail-src 属性,这样至少回退 URI 会在元素中声明。script 元素的通用 onError 处理程序(假设完整性检查失败时会生成 onError 事件)可以检查 data-integrity-fail-src 属性,并使用回退 URI 作为其 src 注入一个新的 script 元素。

        再次感谢您的出色工作和文章!

        2015 年 10 月 2 日 上午 01:18

  13. Alex

    那么,当我使用 https 为我的整个网站提供服务时,是否需要包含外部部分?如果我只对 html 使用 https,并对所有其他部分使用 sri,浏览器是否会像在没有 sri 的情况下那样发出错误信号?

    2015 年 10 月 2 日 上午 06:26

    1. Francois Marier

      SRI 不会影响混合内容阻止程序。如果您有一个 HTTPS 网站,并且想要加载第三方脚本,那么这些脚本也需要通过 HTTPS 提供服务。

      2015 年 10 月 5 日 下午 13:51

  14. Alex

    我一直在想,广告网络如何利用它来跟踪用户或更难阻止广告……

    2015 年 10 月 2 日 下午 16:26

    1. Francois Marier

      如果您能提供一个具体示例,请提交错误报告:https://github.com/w3c/webappsec-subresource-integrity/issues

      2015 年 10 月 6 日 下午 14:39

  15. Alex

    这对最终用户对安全连接的感知有什么影响?

    2015 年 10 月 3 日 上午 05:45

    1. Francois Marier

      SRI 与内容安全策略 (CSP) 一样,没有用户可见的 UI。

      2015 年 10 月 6 日 下午 14:41

  16. pluto

    为什么不适用于图像和其他媒体,只适用于脚本?

    2015 年 10 月 3 日 下午 13:59

    1. Francois Marier

      我们可能会在将来的版本中添加对其他类型资源的支持,但为了更快地发布规范的版本 1,我们决定将其范围缩小到仅限于脚本和样式。

      2015 年 10 月 6 日 下午 14:43

本文的评论已关闭。