Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

如何保证你执行的 JS 就是你想执行的 JS? #13

Open
haochuan9421 opened this issue May 5, 2021 · 0 comments
Open

如何保证你执行的 JS 就是你想执行的 JS? #13

haochuan9421 opened this issue May 5, 2021 · 0 comments

Comments

@haochuan9421
Copy link
Owner

背景

我网站执行的 JS 当然是我自己写的 JS 呀,难不成还有安全问题🤔?但想想,很多时候网站的 JS 是从第三方的 CDN 服务加载下来的,如果CDN服务器受到了攻击,JS 文件被篡改,就会带来安全风险,如何保证我们网站运行的这些脚本文件是未被修改的呢?

还有一种情况就是 html 中可能有一些 inline script,这些 JS 可能是在服务端渲染 html 模版的时候注入的,如果网络传输过程中被抓包篡改了怎么办?

伪代码:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
  </head>
  <body>
    <!-- inject-placeholader -->
    <div id="root"></div>
  </body>
</html>
router.get("/", async (ctx) => {
  const htmlTpl = await getHtmlTpl();
  const script = `window.__SOME_DATA__ = ${JSON.stringify(__SOME_DATA__)};`;
  ctx.type = "html";
  ctx.body = htmlTpl.replace("<!-- inject-placeholader -->", `<script>${script}</script>`);
});

先来看看传统软件是怎么保证用户获取到的内容是未篡改的。比如 linuxkit/linuxkit,他的发布页面会附上 Checksums

image

这些 hash 值是根据文件内容,通过固定的算法得到的,比如上面的 SHA256。这样获取到文件的用户用同样的算法做一次计算,如果得到的 hash 值和作者标明的一致,就可以验证软件没有被别人篡改过的。

回到前端,前端有没有类似的安全方案呢?答案是肯定的。

1. Subresource Integrity(SRI)

我们给 <script><link> 标签添加 integrity 属性,属性值是根据文件内容做 hash 算法得到的一个字符串。形如:

<link href="//somecdn.com/foo.css" rel="stylesheet" integrity="sha256-t7Z7PgokIxRooJ8azMRTqZZIdgaQX6ViGg3pn3pxZZs= sha384-KJi9xVfT8JzG/tFq6Dpgw6URtNE3WK83VaQOWpfHsVAN6Az5+AjliGZuSiiWd4ah" crossorigin="anonymous">

<script src="//somecdn.com/bar.js" integrity="sha256-XXVAhVe8STxZbyQhPOwZpZmx3X9iHnnrBPHUN/4vooc= sha384-438vOegRAvOckkDAIIIL8+k0JhRCRfY7Q2QXLjgFOHQbhyFK/YwGIDJBxYCdaHjA" crossorigin="anonymous"></script>

其中的 sha256sha384 是 hash 算法,而 sha256-sha384- 后面的部分是 hash code, 对于跨域的脚本请求,脚本服务器需要设置 CORS 响应头,允许跨域站点访问他的内容 Access-Control-Allow-Origin: *<script><link> 标签上也需要添加 crossorigin 属性,anonymous 代表请求不携带 cookie。

对于设置了 SRI 的脚本或样式,浏览器会对请求下载的文件内容做同样的 hash 算法,如果 hash code 不匹配,就会拒绝执行。
image

webpack 的项目可以通过 webpack-subresource-integrity 在打包时自动添加 Integrity 和 anonymous,配置如图:

image
image

2. Content-Security-Policy (CSP)

对于一些行内脚本,我们可以通过设置 CSP 的 方式来校验。比如:

router.get("/", async (ctx) => {
  const htmlTpl = await getHtmlTpl();
  const script = `window.__SOME_DATA__ = ${JSON.stringify(__SOME_DATA__)};`;
  const hash = require("crypto").createHash("sha256").update(script).digest("base64");
  ctx.set("Content-Security-Policy", `script-src 'self' 'sha256-${hash}'`);
  ctx.type = "html";
  ctx.body = htmlTpl.replace("<!-- inject-placeholader -->", `<script>${script}</script>`);
});

上面的 CSP 设置保证了只有 hash 值和我们给定的 hash 值一致 inline-script 才会被执行,对于不符合的 inline-script,会抛出如下错误:

image

不过话说回来,既然算法是固定的,交付 hash code 的过程也是通过网络传输到前端的(html 内容,response header),如果有人能在网络传输的过程中修改响应,那么他把恶意代码做一次同样的算法,然后把 hash code 也改掉,还是无法避免不安全脚本的出现。这就要求我们要保证网络传输的安全。一方面我们需要给网站配置 https 避免明文传输,另一方面也要告知用户不要在访问网站的时候使用不可信代理。

补充

顺便说一下,保证内容可靠性的另一种常见做法。对接过微信公众号消息的同学可能有印象,微信的服务器会给我们的服务器推送消息,我们如何保证消息是微信的服务器发送过来的,而不是其他人伪造的呢?做法就是微信和我们自己的服务器都持有同一个 Token。微信发过来的消息中有一个签名 signature,这个签名就是由请求的内容和这个 Token 共同参与生成的。只要保证微信侧和我们自己的服务持有同一个Token,对同样的内容,做同样算法的签名就可以,如果最终得到的签名一致就说明请求确实是微信发给我们的。可以参考我之前写过的 Node.js 对接微信消息通知的 [ 示例

image

这种做法不同于上面的一点是双方都保留了一个不为第三方所知的 Token,只要 Token 没泄露,即使别人知道你们的签名算法是啥,也无法伪造出一个签名。这种方式并不适用于前端,因为前端无法不通过网络把一个 “Token” 预先埋在所有用户的系统里。

参考链接

https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
https://developer.mozilla.org/zh-CN/docs/Web/HTML/CORS_settings_attributes
https://github.com/waysact/webpack-subresource-integrity
https://webpack.js.org/configuration/output/#outputcrossoriginloading
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant