如何证明客户端 Javascript 是安全的?

信息安全 加密 javascript 服务器 客户端 客户
2021-09-03 20:01:26

想象一下,您有一个 Web 应用程序,它在服务器和客户端上都对用户的数据(例如便笺或电子表格)进行加密。

用户使用这个 Web 应用程序的正常过程是这样的:

  1. 用户使用存储在服务器上的登录名/密码哈希登录应用程序。(就像普通的 Web 应用程序一样。)
  2. 用户输入用于加密客户端数据的附加安全密钥。Web 应用程序使用客户端加密库,例如SJCL

在这个例子中,让我们只关注客户端。

情况是这样的:服务器已经被攻破,攻击者访问了服务器端的密钥。攻击者没有客户端密钥,因为它们从未存储在服务器上。

现在,当用户在 Web 应用程序(客户端)中输入客户端密钥时,攻击者需要修改 Javascript 以读取客户端密钥。Javascript 将被编程为将密钥发送给攻击者/服务器。现在攻击者赢了。

我知道假设一旦你接管服务器,你就输了,但我想知道我下面的想法是否允许客户端安全解决方案。


情况

假设 HTML 在一些脚本标签中包含一些 Javascript 代码,并且还有许多通过驻留在服务器上的外部 Javascript 文件加载的 Javascript 代码。问题在于运行 Web 应用程序的 Javascript。我们必须假设攻击者修改了任何 Javascript,无论是内联的还是外部的。

可能的解决方案?

我希望能够生成从我的服务器加载的所有 Javascript 的哈希值。has 将充当客户端 Javascript 代码的指纹,用户将对新的哈希保持警惕。

到目前为止,这是我想到的两种方法:

  1. 获取加载到客户端的所有文件的哈希值。这意味着再次请求所有包含的文件。

  2. 对内存中的所有 Javascript 代码进行哈希处理。(这甚至可以做到吗?)

这两个选项的共同问题是,无论实际执行此哈希的函数是什么,它都需要足够小,以便相关用户可以在几秒钟内验证它是否可以安全使用。

我在想这个散列函数像往常一样加载到浏览器中,用户可以从控制台输入函数名称而不用,()这样他们就可以看到代码,然后再次输入 with()来运行代码。

然后哈希应该足以证明 Web 应用程序处于用户知道他们过去检查过的状态。

这甚至可能在某个时候成为一个插件,尽管我决心看看是否有可能使用原生解决方案。


本质上我要问的是,存在哪些方法可以让我们证明客户状态的完整性?

4个回答

你不能确定它没有被篡改。攻击者正在您的系统上运行代码 - 只要付出足够的努力,他们就可以操纵您正在运行的浏览器上下文中发生的任何事情(因此,插件不会受到同样的影响 - 它位于不同的上下文中)。

并非所有来自@SmokeDispenser 的 Matasano 链接中的点都完全正确,尽管基本原则仍然存在。诸如WebCrypto API之类的努力正试图解决一些问题,但还不成熟——即使它们成熟了,也无法确定代码在执行的同时没有做恶意的事情预期的行为。

包含 JavaScript 的网页本质上是一个在您计算机上的沙箱中运行的小型应用程序。每次访问该页面时,您都会下载最新版本的应用程序并运行它。强制性XKCD漫画

这意味着,如果攻击者控制了您的服务器并且可以提供中毒代码,那么您的问题与您的用户从狡猾的下载站点下载了带有间谍软件的软件版本非常相似。 您插入应用程序的任何保护措施都可以被攻击者删除或绕过。

保护 Web 应用程序免受控制服务器的攻击者的唯一方法是,如果您的 Web 应用程序的某些部分存储在用户的计算机上。例如,这可能是下载的文件或data:URL 书签。这段代码将首先被加载,然后可以包含足够的逻辑来在执行之前检查所有附加资源的完整性——例如,通过子资源完整性或在旧浏览器中在使用之前验证哈希值exec()

(我写了一个小的sha256 实现来玩这个从data:URL 引导的想法,甚至是一个基于它的模块加载器,但显然不建议在生产中实际使用它。)

简而言之:如果您希望您的用户只输入一个 URL 并加载您的网站,那么这完全取决于服务器的安全性。如果攻击者只针对特定用户,即使监控您自己的网站也可能对您没有帮助。

如果我的理解正确,您希望确保服务器提供的代码与客户端上的某些公认良好的概念相匹配。但是对于浏览器来说,唯一可以向浏览器提供内容的地方是服务器——所以你的验证方式是从与你想要验证的内容相同的来源和相同的渠道传递的(正如 Matthew 所说)。

如果您可以将两部分交付给客户端的时间分开(即使用不同的缓存时间,并让每一部分验证另一部分),则可以利用这一点来发挥您的优势。但它远非万无一失。

Javascript 提供了足够的反射来直接进行验证(是的,您可以读取 Javacript 内存中的内容)。问题在于区分作为页面一部分出现/由页面加载的代码和已经内置在浏览器中的代码。后者会因品牌和版本而异。只要您的代码调用浏览器提供的代码(例如在屏幕上写东西),您也需要能够验证浏览器代码。这是一个问题,因为用其他东西替换任何 javascript 函数(包括内置函数)很简单:

_orig_write = document.write;
document.write = function (str) {
    send_data_to_evil_site(str);
    _orig_write(str);
}

您不能依赖检测:

if ('function write() { [native code] }' != document.write.toString()) {
     alert("maybe the toString was changed too?");
}

您可能想看看在签名的 jar 文件中传输您的 javascript 。虽然最初是为了在其沙箱之外提供 Javascript 访问,但浏览器内置的用于验证内容的机制应该比本土解决方案更强大 - 但请再次记住,此代码可能会在沙箱之外产生影响(可能是对任何有安全意识的客户来说都是关闭的)。

即使您的服务器端代码没有受到损害,验证客户端代码也是有意义的。如果攻击者能够修改代码或注入新代码,他可以轻松捕获凭据或修改页面标记并进行网络钓鱼,这足以让人们担心。

关于迄今为止提出的解决方案:

  • 子资源完整性 - 仅验证第三方代码的完整性,然后仅在加载时验证。攻击者可以内联注入代码或毒害现有代码。因此,SRI 对此类特定攻击无效。它旨在检测您的 CDN 何时被入侵。
  • WebCrypto - 在浏览器中拥有标准加密是很好的,但就像任何其他可用的本机功能一样,它可能会中毒。
  • 提出的其他解决方案依赖于他们的代码首先被执行而不是可能的对手。问题是这很难保证。这就是为什么像 CSP 这样的标准在 HTTP 标头中携带,并且浏览器根据定义在加载任何 JS 之前首先执行它们。(顺便说一句,CSP 也不能防止代码中毒)。

没有防弹解决方案。您可以做的是尽可能提高标准,以减轻大多数攻击并削弱其他攻击。

我很惊讶没有人建议 JavaScript 混淆。如果混淆具有足够的弹性甚至是多态的,它可以生成难以理解且足够多样化的输出。您可以定期轮换受保护的版本以实现此目的。这样您就可以消除自动中毒目标,因为代码的名称和形状甚至布局都在不断变化。我假设攻击者远离浏览器(因此需要自动化攻击)。此外,今天有一些解决方案可以生成自我防御代码,使代码能够抵抗篡改和中毒,这使得破解变得越来越复杂。

要专门处理对 DOM 的修改,您需要稍微不同的东西来检测这些修改并删除它们。