Web页面子资源SRI完整性校验


2021-12-19 安全

Subresource Integrity (SRI),表示子资源完整性。是允许``浏览器**检查**其获得的资源`(例如从 CDN 获得的)是否被篡改的一项安全特性。

  • SRI校验方式: 通过验证获取文件哈希值是否和你提供的哈希值一样来判断资源是否被篡改。

譬如在页面中通过 link 和 script 标签引入的样式文件或者引入使用的第三方库就是页面的的子资源。如:

<script
  src="https://example.com/example-framework.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"
></script>
1
2
3
4
5

标签中的 crossorigin="anonymous" 是表明这个资源的请求是需要跨源资源共享的。

标签中的 integrity 是为了启用浏览器子资源完整性功能检查。

integrity 中 sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC 就是 integrity 的值,这个字符串以 sha384 开头,表示的是对应安全散列算法的名称。

integrity 值分成两个部分,两部分通过一个短横-分割:

  • 第一部分指定哈希值的生成算法(目前支持 sha256、sha384 及 sha512)
  • 第二部分是经过 base64 编码的实际哈希值。

# SRI 解决啥问题

一般情况下,为了提高网页的响应速度以及性能,通常会把这些子资源放到 CDN 上。对于大厂来说基本会有自己的 CDN 服务。但是对于小公司来说,一般会使用云服务厂商提供的 CDN 功能。这里就会有一个问题,如果我们托管在云服务厂商的 CDN 上的资源万一被篡改了,那么就会对我们的业务产生一些影响。虽然这种事情一般情况下不会发生,但是如果我们的业务对安全要求很高的话,那么还是要对这种情况做好防范处理。

SRI 就是应对这个问题的一个解决方案。那么具体是通过什么方式来解决的呢?首先对于一个文件,我们如何知道这个文件的内容有没有被篡改呢?我们可以对这个文件进行一个哈希计算然后通过 base64 编码生成一段跟文件内容关联的唯一的字符串。如果文件的内容发生了变化,那么通过相同的方式生成的字符串,跟原来的文件生成的字符串是不一样的。这样我们就知道文件被篡改了。

当浏览器下载了带有 integrity 属性的子资源的时候,不会立刻执行子资源里面的代码。浏览器会首先根据 integrity 属性值中指定的相应算法以及下载的文件的内容计算一下这个文件的哈希值是否跟标签中的那个值一样,只有两者一样的情况下才会应用对应的样式或者执行相应的代码。如果两者不一样,那么浏览器就会拒绝执行对应的代码,以及拒绝应用对应的样式。也会在控制台报错,提醒我们当前下载的子资源存在问题。

这样我们就通过 SRI 这种方法保证了我们的页面不会使用从 CDN 上下载的被篡改了内容的资源。保证了我们的页面的安全。

# 使用 SRI

那么具体怎么实践呢?下面我们来一起实践一下如何使用 SRI。

首先创建一个测试 demo:

<!-- index.html -->
<script
  src="./test.js"
  integrity="sha384-yGduQba2SOt4PhcoqN6zsgbwhbpK8ZBguLWCSdnSRc6zY/MmfJEmBguDBXJpvXFg"
  crossorigin="anonymous"
></script>
1
2
3
4
5
6
/* test.js */
document.write('Hello World!');
1
2

然后在本地使用 Node.js 将 demo 跑起来。

接着就是设置 script 标签的 integrity 属性与属性值。

可以用 openssl 在命令行中执行如下命令来生成 SRI 哈希值:

cat test.js | openssl dgst -sha384 -binary | openssl enc -base64 -A
1

或者用 shasum 在命令行中执行:

shasum -b -a 384 test.js | xxd -r -p | base64
1

还可以通过 SRI 哈希值的工具SRI Hash Generator在线生成 。

接着开发 index.html 页面一切正常,页面上展示:Hello World!了。

如果我们这个时候把 test.js 的内容更改一下,在原来的基础上,把 World!后面的感叹号去掉,如下所示:

/* test.js */
document.write('Hello');
1
2

此时刷新页面发现不显示内容了,打开开发者工具显示报错如下:

<!-- Chrome -->
Failed to find a valid digest in the 'integrity' attribute for resource 'http://192.168.43.32:5500/code/SRI/test.js' with computed SHA-256 integrity 'G7+3yE5p+R47YX7+s8WfLo/GnGaXh77CDlNBw6RcVH8='. The resource has been blocked.
1
2

此时说明了下载的子资源经浏览器计算后得到的哈希字符串,跟 integrity 标签上的值不一致所致,浏览器拒绝执行对应的代码。

另外还有一些需要注意的地方,如 test.js 资源跟 index.html 是否源,如果不同源还需要在标签上添加 crossorigin="anonymous",表明这个资源的请求是需要跨源资源共享的,不然浏览器会报错跨域错误。

# integrity的一些细节

在实际的使用过程中,还有很多细节需要注意的,下面给大家再深入的介绍一下。

  • 目前使用计算资源文件哈希值的算法有 sha256,sha384,sha512,这些都是属于 SHA-2 的安全散列算法。

  • 目前已经不推荐使用 MD5SHA-1 的计算哈希值的算法。

  • integrity 的值可以存在多个,每个值之间使用空格分隔

    • 如果多个值分别使用的是不同的安全散列算法,比如:
    <script
      src="./test.js"
      crossorigin="anonymous"
      integrity="
          sha256-LsK9lSOT7mZ9iEbLTm9cwaKTfuBdypNn2ID1Z9g7ZPM=
          sha384-yGduQba2SOt4PhcoqN6zsgbwhbpK8ZBguLWCSdnSRc6zY/MmfJEmBguDBXJpvXFg
          sha512-2qg2xR+0XgpsowJp3VCqWFgQalU9xPbqNTV0fdM9sV9ltHSSAcHni2Oo0Woo6aj860KvFu8S1Rdwb8oxJlMJ2Q==
    "
    ></script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    那么这个时候浏览器是根据那个安全散列算法来进行处理的呢?还是说只要有一个匹配就可以了呢?

    答案是:浏览器首先会选择安全性最高的那个计算方式,如果是上面这个例子的话,浏览器会选择 sha512 这种计算哈希值的算法。因为 sha512 的安全性大于 sha384,sha384 的安全性大于 sha256,然后会忽略掉其余通过其他方式计算出的哈希值。这个时候需要注意的是,如果浏览器根据 sha512 计算出来的哈希字符串跟提供的不一样的话,那么不管 sha384 或者 sha256 提供的哈希值是否正确,浏览器都会认为这个资源计算出来的哈希值跟提供的哈希值不一样。所以不会执行对应的代码。

    • 如果多个值分别使用的是相同的安全散列算法,那么这个时候只要有一个值跟浏览器计算的结果是一样的,那么这个资源就可以被认为是没有被篡改的;资源的内容是可以被执行的。比如:
    <script
      src="http://localhost:3000/test.js"
      crossorigin="anonymous"
      integrity="
          sha384-yGduQba2SOt4PhcoqN6zsgbwhbpK8ZBguLWCSdnSRc6zY/MmfJEmBguDBXJpvXFg
          sha384-c+xXeW2CdZ1OuDKSrMpABg4MrVFWi3N5VKDC6CTgSRRnPr0dgprowjuFPomHgXlI
          sha384-E6ULLMoeKAMASZMjQ00AvU+3GzK8HPRhL/bM+P4JdcHLbNqGzU14K9ufSPJCnuex
    "
    ></script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  • integrity 属性暂时只支持 link 和 script 标签且存在兼容性差异,以后会支持更多的关于子资源的标签,比如:audio, embed,iframe,img 等。

# 在框架中使用 SRI

# Vue.js

  • Vue CLI 项目

在 vue.config.js 中增加一个配置:integrity: true

build 项目后 index.html 中引入的资源都是带有 integrity 属性的,如下:

<!-- ... -->
<link
  href="/css/app.fb0c6e1c.css"
  rel="stylesheet"
  integrity="sha384-1Ekc46o2fTK9DVGas4xXelFNSBIzgXeLlQlipQEqYUDHkR32K9dbpIkPwq+JK6cl"
/>
<!-- ... -->
<script
  src="/js/chunk-vendors.0691b6c2.js"
  integrity="sha384-j7EDAmdSMZbkzJnbdSJdteOHi77fyFw7j6JeGYAf4O20/zAyQq1nJ91iweLs6NDd"
></script>
<script
  src="/js/app.290d19ae.js"
  integrity="sha384-S3skbo1aIjA4WCmQH6ltlpwMgTXWrakI5+aloQEnNKpEKRfbNyy1eq6SrV88LGOh"
></script>
<!-- ... -->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# Webpack 自建打包

使用 webpack 对应的插件包: webpack-subresource-integrity

# 相关文献

Loading comments...