什么是SRI

image-1659956791367

SRI是Subresource Integrity的简写,表示的是子资源的完整性。那么什么又是子资源呢,比如我们常见的页面中会引入各种第三方脚本库。

<link rel="stylesheet" 
      href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css"  
      crossorigin="anonymous">

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js" 
        crossorigin="anonymous"></script>

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

如何使用SRI

上面简单介绍了SRI的作用,那么具体怎么实践呢?下面我们来一起实践一下如何使用SRI。

首先我们随便创建一个index.html,然后在里面添加如下内容:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>SRI-demo</title>
</head>
<body>
<h1>SRI-demo</h1>
<script src="http://localhost:3000/test.js"
        integrity="sha384-yGduQba2SOt4PhcoqN6zsgbwhbpK8ZBguLWCSdnSRc6zY/MmfJEmBguDBXJpvXFg"
        crossorigin="anonymous"></script>
</body>
</html>

然后创建一个test.js文件,里面内容如下:

document.write("Hello World!");

然后在本地使用Node.jsexpress框架或者其他的工具,让test.js能够在本地通过http://localhost:3000/test.js访问。

对于上面script标签的integrity属性,我们可以通过如下的命令,通过openssl工具获取对应的sha384算法生成的字符串:

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

如果是Windows环境的话,需要使用另外的方式获取对应的字符串。

然后在浏览器中打开index.html,你会看到页面上展示:Hello World!。如果我们这个时候把test.js的内容更改一下,在原来的基础上,把Hello World!后面的感叹号去掉,如下所示:

document.write("Hello World");

那么这个时候页面就是空白的,不再展示Hello World。对应的控制台也会报错,不过不同的浏览器报错信息不一样:

Chrome报错如下:

image-1659956655489

总之都会提醒你,当前下载的子资源通过计算后的哈希字符串,跟标签上的不一致,浏览器拒绝执行对应的代码。

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

image-1659956662885

如果对跨源资源共享还不是很明白的同学可以参考Cross-Origin Resource Sharing

当然对应的服务端也需要设置对应的响应头部:Access-Control-Allow-Origin: *,如果是使用express的话,可以使用cors来简单的设置一下。具体如下所示:

app.use(
    cors({
        origin: "*",
    })
);

如何在框架中使用SRI

  • 对于Vue项目来说,通过使用Vue CLI我们可以很简单的就使用这个功能。通过在vue.config.js中增加一个配置:integrity: true,我们就可以在构建的时候后看到,打包后的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>
  • 对于其他框架来说,如果打包工具使用的是Webpack的话,可以直接使用对应的插件webpack-subresource-integrity,相关的安装和使用说明可以参考这里。

关于Integrity的一些细节

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

  • 目前使用计算资源文件哈希值的算法有sha256sha384sha512,这些都是属于SHA-2的安全散列算法。
  • 目前已经不推荐使用MD5SHA-1的计算哈希值的算法。
  • 首先Integrity的值可以存在多个,每个值之间使用空格分隔开。
  • 如果多个值分别使用的是不同的安全散列算法,比如如下所示:
<script src="http://localhost:3000/test.js"
        crossorigin="anonymous"
        integrity="
        sha256-LsK9lSOT7mZ9iEbLTm9cwaKTfuBdypNn2ID1Z9g7ZPM=
        sha384-yGduQba2SOt4PhcoqN6zsgbwhbpK8ZBguLWCSdnSRc6zY/MmfJEmBguDBXJpvXFg
        sha512-2qg2xR+0XgpsowJp3VCqWFgQalU9xPbqNTV0fdM9sV9ltHSSAcHni2Oo0Woo6aj860KvFu8S1Rdwb8oxJlMJ2Q==
"></script>

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

答案是:浏览器首先会选择安全性最高的那个计算方式,如果是上面这个例子的话,浏览器会选择sha512这种计算哈希值的算法。因为sha512的安全性大于sha384sha384的安全性大于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>

那么这个时候只要有一个值跟浏览器计算的结果是一样的,那么这个资源就可以被认为是没有被篡改的;资源的内容是可以被执行的。

  • Integrity属性暂时只支持linkscript标签,以后会支持更多的关于子资源的标签,比如:audioembediframeimg等。