目录

使用SRI保护你的网站免受第三方CDN恶意攻击

出于速度和降低服务器负载考虑,有时候我们会选择使用 CDN 加载第三方静态资源。对于一些热门的第三方库,在用户打开你的网页之前就很有可能在浏览别的网站时被浏览器缓存下来,这样就可以极大的提升网页加载速度。

然而使用 CDN 也提高了网站的安全风险:第三方静态资源放在第三方服务器上,CDN 的拥有者有没有可能偷偷的篡改这些文件,加入恶意代码呢?或者 CDN 服务器遭受了黑客攻击,整个文件被替换掉。虽然可能性不高,但不是零。JavaScript 对于当前浏览器页面有完全控制权,他们不仅仅能获取到页面上的任何内容,还能抓取用户输入的一些诸如密码之类的机密信息,还能获取到保存到 Cookie 中的登录票据等等内容,这就是所谓的 XSS 攻击。

我们需要一种机制确保从 CDN 下载的文件未被恶意篡改。某些下载网站就提供下载文件的 MD5 或 SHA1 码用于检查所下载文件的完整性,网页中有没有类似的机制呢?

What(什么是 SRI)

Subresource Integrity (SRI) is a security feature that enables browsers to verify that files they fetch (for example, from a CDN) are delivered without unexpected manipulation.

和 link 标签中通过 integrity 属性,浏览器核实所获取的 js 文件(或 css 文件)确实是如 integrity 值规定的,然后再加载 js 文件(或应用css 文件)。

例子 如下是个 script 标签

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

注意 integrity=“sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC”,integrity 的值以 sha384- 开头,表示算法为 sha384, dash (-) 之后跟随的是 base64-encoded hash。

当前所允许的 hash 算法有 sha256, sha384, and sha512

生成 SRI Hashes值

命令行有两种方法。

方法一

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

方法二

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

注意,这里 shah 算法是 sha384,如果生成其他的 hash 值,是否也像这样,只需稍作修改即可(可能吧,但是未验证)。

方法三

1
$ echo -n "alert('Hello, world.');" | openssl dgst -sha384 -binary | openssl base64 -A

使用了 OpenSSL 这个 *nix 中通常都包含的工具计算哈希值。其中 alert('Hello, world.'); 是文件内容,你也可以用 cat Filename.js 直接读取某个文件。

输出 H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO,在此基础上添加前缀 sha384- 就可以了。

还有在线工具 https://srihash.org/

可以生成不同格式的工具 https://www.xftsoft.com/tool/integrity

浏览器如何处理 SRI (Subresource Integrity)

  1. 当浏览器遇到一个带有 integrity 的 <script> 或 <style> 标签,在执行其中的 JS 脚本或应用其中的 CSS 样式之前,浏览器会首先计算所下载文件的内容的哈希值是否与 integrity 属性给定的值相同。

  2. 如果计算结果与给定值不匹配,浏览器会拒绝执行脚本内容,并报出一个网络错误,类似如下结果:

  3. When a browser encounters a <script> or <link> element with an integrity attribute, before executing the script or before applying any stylesheet specified by the <link> element, the browser must first compare the script or stylesheet to the expected hash given in the integrity value.

  4. If the script or stylesheet doesn’t match its associated integrity value, then the browser must refuse to execute the script or apply the stylesheet, and must instead return a network error indicating that fetching of that script or stylesheet failed.

Failed to find a valid digest in the ‘integrity’ attribute for resource ‘https://cdnjs.cloudflare.com/ajax/libs/normalize/6.0.0/normalize.min.css' with computed SHA-256 integrity ‘VbcxqgMGQYm3q8qZMd63uETHXXZkqs7ME1bEvAY1xK8=’. The resource has been blocked.

参考 Subresource Integrity https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity

使用 SRI

只需给 scriptstyle 标签添加 integrity 属性即可。例如:

  • JavaScript
1
<script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha384-xBuQ/xzmlsLoJpyjoggmTEz8OWUFM0/RC5BsqQBDX2v5cMvDHcMakNTNrHIW2I5f" crossorigin="anonymous"></script>
  • CSS
1
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css" integrity="sha384-7tIwW4quYS2+TZCwuAPnUY+dRqg28ylzlIoVXAwpfiTs+CMKsAOSsWYQ96c/ZnV+" crossorigin="anonymous">

integrity 属性值以 shaXXX- 开头,表示后面的哈希值使用的哈希算法,目前只允许 sha256sha384sha512 这三种哈希算法,以 sha384 比较多见。后面跟对应的哈希值即可。

值得注意的是,因为启用 SRI 需要获取所下载文件的内容进行计算,所以需要 CDN 服务器启用跨域资源访问(CORS)支持,即返回 Access-Control-Allow-Origin: * 头。客户端需要使用跨域的形式加载指定文件,即添加 crossorigin="anonymous" 属性。就我所知,目前国内相对常用的免费 CDN bootcdn 已经支持 CORS,百度静态 CDN 还不支持。

CSP 与 SRI

你可以使用 内容安全政策CSP)强制要求当前页面所有脚本加载标签启用 SRI。例如

1
Content-Security-Policy: require-sri-for script;

强制要求所有 script 标签启用 SRI,浏览器会拒绝加载未启用 SRI 的 script 标签。

对应的还有 CSS 版本:

1
Content-Security-Policy: require-sri-for style;

你也可以同时启用两者。

错误恢复

使用 CDN 时别忘了当尝试从 CDN 加载文件失败后加载本地版本:

1
2
3
4
<script src="https://code.jquery.com/jquery-3.2.1.min.js"
        integrity="sha384-xBuQ/xzmlsLoJpyjoggmTEz8OWUFM0/RC5BsqQBDX2v5cMvDHcMakNTNrHIW2I5f"
        crossorigin="anonymous"></script>
<script>if (!window.jQuery) document.write('<script src="/jquery-3.2.1.min.js"><\/script>')</s