⚠️ 实验性 [WIP] - 使用风险自负(了解更多)
试试 LavaDome - 访问演示应用,打开控制台,尽你所能从 LavaDome 实例中窃取秘密(报告你的成功)
在当今的 Web 标准下,还没有一种成熟的方法能够以安全的方式选择性地隔离 DOM 子树。换句话说,如果各方共享同一个 JavaScript 执行环境,我们无法通过向某些方授予访问权限、同时阻止其他方访问,来控制对 DOM 各部分的访问。
我们生活在一个再也无法信任自己应用中的代码的世界,同源执行并不能保证安全。要在前端保护秘密,我们必须能够向用户呈现内容,同时确保其不会被同源运行的 JavaScript 代码窃取。

目前,这些敏感内容一旦导出就会被直接附加到 DOM 上,使其对所有在同一个应用中运行的实体完全可访问。也就是说,那些本不应访问私钥的代码部分,只要恶意代码能够访问 DOM,就可以轻松地以明文形式提取私钥。
但请放心。我们相信这是一个可解决的问题 👇
LavaDome 目前支持 Vanilla JavaScript 和 React(更多功能正在开发中)
import { LavaDome as LavaDomeJavaScript } from '@lavamoat/lavadome-javascript';
const root = document.getElementById('root'); const lavadome = new LavaDomeJavaScript(root); lavadome.text(secret); lavadome.copy(); // copy to clipboard
### [React](https://github.com/lavamoat/lavadome/blob/main/packages/react)```javascript
import { LavaDome as LavaDomeReact, toLavaDomeToken } from '@lavamoat/lavadome-react';
function Secret({ text }) {
const {token, copy} = toLavaDomeCapabilities(text);
return <>
<a onClick={copy}> copy to clipboard </a>
<LavaDomeReact token={token} />
</>;
}
除了根节点之外,所有构造函数都接受可选的第二个参数 options:```javascript // javascript new LavaDomeJavaScript(root, { // boolean unsafeOpenModeShadow: false, });
// react function Secret({ text }) { const {token} = toLavaDomeCapabilities(text); return <LavaDomeReact token={token} // boolean unsafeOpenModeShadow={false} /> }
### 安全使用
由于 Web 核心的限制,为了安全地集成 LavaDome,集成开发者需要注意一些事项,这些事项需要开发者积极投入:
#### 执行顺序
LavaDome 与任何其他 JavaScript 安全软件一样,始终容易受到在其之前运行的代码的攻击。
这意味着,除了我们完全信任的代码之外,LavaDome 必须是 Web 应用程序中加载的第一段代码。
虽然这并不意味着开发者必须立即使用它(而是只在需要时使用),但他们确实必须尽早引入该程序。
为了正确(安全)地做到这一点,它必须是整个程序中第一个 import/require 声明:```javascript
import '@lavamoat/lavadome-react';
import 'other-stuff';
console.log('Program starts here');
这样我们可以确保 LavaDome 有足够时间做好自身准备,以保障安全使用。
请注意,这同样适用于 LavaDome 的其他软件包,而不仅仅是 @lavamoat/lavadome-react(因此无需导入其中多个)。
跳转到 安全(防御性编码) 了解更多信息。
由于侧信道攻击和 Web 限制,导入远程字体可能成为针对 LavaDome 的有效攻击手段。由于该问题植根于 CSS 领域,目前无法通过 LavaDome 本身来解决。
幸运的是,这可以通过 CSP 的 font-src 指令有效解决。
为缓解此类攻击,请确保你的 Web 应用不允许从未知服务器获取字体。
跳转到 安全(侧信道) 了解更多信息。
开发人员提供给 LavaDome 的文本必须 100% 不可预测,否则可能遭到攻击并导致泄露。
因此,如果你的应用需要显示 "your key is 234789",这意味着你的 DOM 结构应为:```html
your key is 234789
并且不能是:```html
<span> <lavadome>your key is 234789</lavadome> </span>
跳到安全(可发现性)了解更多。
将 LavaDome 集成到测试环境中可能会有些棘手,因为 LavaDome 在隐藏秘密方面做得很好,它也会很好地瞒过你的测试!
要成功将 LavaDome 集成到你的测试环境,你可能需要 @lavamoat/lavadome-core 导出的 LavaDomeDebug 的帮助:```javascript
// IMPORT/USE FOR TESTING/DEBUGGING PURPOSES ONLY - NEVER IN PRODUCTION!
import { LavaDomeDebug } from '@lavamoat/lavadome-core';
以下是 `LavaDomeDebug` 导出的一些调试工具方法,可帮助您测试基于 `LavaDome` 的组件:
#### `getTextByRoot()`
给定一个已挂载 `LavaDome` 的根节点,`getTextByRoot()` 会递归提取并重构内部秘密。要实现这一点,`LavaDome` 实例最初必须使用不安全的选项 `@unsafeOpenModeShadow` 初始化,这会使 `LavaDome` 的内部影子可从外部访问。
显然,这是不安全的,会让 `LavaDome` 完全暴露在风险中,但仅用于测试/调试目的时是有意义的——请务必确保切勿在生产环境中启用此选项!```javascript
new LavaDomeJavaScript(root, {
unsafeOpenModeShadow: isThisTestingEnv, // boolean
}).text('123456');
LavaDomeDebug.getTextByRoot(root) === '123456'; // true
stripDistractionFromText()当使用 Web 驱动进行测试并指示它们提取 LavaDome 实例根节点的内部文本时,它们会返回一个同时包含秘密和 LavaDome 干扰文本的字符串。
干扰文本对于安全性很重要(参见 安全(侧信道)),但它会让 Web 驱动提取并非真正属于秘密的字符。
为了解决这个问题,在获得 Web 驱动返回的文本后,stripDistractionFromText() 会从中移除干扰文本,只留下你的测试期望找到的精确字符串。
不用担心干扰文本,它在你的应用中永远不会对用户可见/可交互,但出于安全原因它必须存在。```javascript new LavaDomeJavaScript(root).text('123456'); const element = await driver.findElement('#ROOT'); // driver const text = await element.getText(); // driver LavaDomeDebug.stripDistractionFromText(text) === '123456'; // true
## 开发
要设置 **`LavaDome`** 的本地开发构建,请克隆此仓库并运行以下任一命令:```bash
npm install && npm install --global serve
列出会议使用的所有 IP 地址。提供会议密码、密码哈希、用户名、音频、视频和聊天记录。
域仿冒和BITB攻击现已就绪。在 gophish 中,你可以创建指向 Zoom 域仿冒的电子邮件模板。设置完成后,你可以在 gophish 启动器中使用 URL 仿冒,从而实施浏览器内中间人攻击。查看所有未入侵 Zoom 会议的用户。你拥有他们的摄像头、麦克风和屏幕共享流。
您的计算机必须能够在网络接口上看到真实域名解析为外部 IP(例如互联网),或者您的内部 DNS 必须指向 ZC2 服务器的 IP。
如何设置:
--domain zoom.yourdomain.com。以下是一个启动示例,使用默认自签名证书,并在需要时启用邮件发送功能:
./ZC2 --meeting-id 1234567890 --meeting-password 123456 --mail-from [email protected] --mail-pass mypassword123 --mail-smtp smtp.gmail.com --mail-smtp-port 587 --user-pass Rand0mP4ss!! --chat capture --stream-record --db-file ZC2.db --listen-all-ip --listen-port 443 --domain zoom.myorg.com
解释:
./ZC2--meeting-id 1234567890 - 会议 ID--meeting-password 123456 - 会议密码--mail-from [email protected] - 邮件发件人地址--mail-pass mypassword123 - 邮件密码--mail-smtp smtp.gmail.com - 邮件 SMTP 服务器--mail-smtp-port 587 - 邮件 SMTP 端口--user-pass Rand0mP4ss! - 创建用户所需的密码--chat capture - 捕获聊天日志--stream-record - 录制会议流--db-file ZC2.db - 数据库文件名--listen-all-ip - 在所有 IP 上监听--listen-port 443 - HTTPS 监听端口这将启动服务器,所有用户必须访问 https://zoom.myorg.com 并输入密码 Rand0mP4ss!。一旦输入完整会议 ID 和密码并登陆,所有用户都将在同一个虚拟会议中看到彼此(在 Zoom 中不可见),这时候你将
用户将看到要求授予摄像头和麦克风权限的提示。不要接受! 如果你是会议安全测试人员,或者你在攻击,那么你将在具有 IP 地址的远程计算机上隐身(加入真实 Zoom 会议时)。你可以保持摄像头和音频关闭,只观察其他用户。你还可以记录他们的摄像头流和音频流。在隐身观察/记录时,你的浏览器标签页必须始终保持活跃和前台状态。
待用户加入会议后,你可以在命令行界面执行 users 查看服务器上的活跃用户:```bash
yarn install && yarn global add serve
## Solution
[`ShadowDom`](https://web.dev/articles/`ShadowDom`-v1) Web API 使我们能够隔离和封装 DOM 节点。虽然它[并非被设计为安全特性](https://web.dev/articles/`ShadowDom`-v1#:~:text=Note%3A%20Closed%20shadow%20roots%20are%20not%20very%20useful.%20Some%20developers%20will%20see%20closed%20mode%20as%20an%20artificial%20security%20feature.%20But%20let%27s%20be%20clear%2C%20it%27s%20not%20a%20security%20feature.for%20Closed%20mode%20simply%20prevents%20outside%20JS%20from%20drilling%20into%20an%20element%27s%20internal%20DOM.),`ShadowDom` 在将 DOM 子树与页面中其他位置运行的 JavaScript 和 CSS 隔离方面表现出色。
**`LavaDome`** 的基本方法是利用 `ShadowDom`,同时仔细处理其[潜在安全漏洞](https://blog.ankursundara.com/shadow-dom/)。
**[LavaDome](https://github.com/lavamoat/lavadome/)** 旨在成为 **LavaMoat 工具箱中的一款安全工具**,用于实现仅允许与用户和受信任代码交互的前端组件,同时阻止应用中不受信任的 JavaScript 和 CSS 代码的访问尝试。
> 感谢 [@arxenix](https://github.com/arxenix) 对 `ShadowDom` 安全性的[研究](https://blog.ankursundara.com/shadow-dom/),这些研究为 **`LavaDome`** 中实现的主要安全改进提供了基础。
## 目标
**`LavaDome`** 项目遵循以下核心原则:
### 安全
我们的首要任务是提供严密的安全性。我们用高级安全属性包装了 `ShadowDom` API,使其在呈现敏感信息时可以安全使用。
请访问 [安全](#Security) 以了解更多有关这项工作的信息。
### 开发者体验
我们致力于提供流畅的开发者体验。为此,我们将:
1. 尽可能支持多种流行框架(React、Angular 等);
2. 使 API 易于使用且简单明了。
### 只读模式
现阶段,我们不打算支持写入模式,这意味着 **`LavaDome`** 仅接受纯文本内容进行保护,而不接受更复杂的内容。
这是因为支持写入模式需要实现一个棘手的隔离 DOM,这会带来多个我们目前尚未准备好应对的安全复杂问题,例如:
1. 事件监听器安全——防止外部代码拦截发往 LavaDome 内部节点的输入。
2. 覆盖层安全——防止恶意代码在 **`LavaDome`** 上放置钓鱼 DOM,诱使用户将敏感输入提供给错误的实体。
## 设计
该项目的设计复杂度并不高。然而,同时满足其安全原则的组合要求是一项并非易事的任务(参见[安全](#Security))。
**`LavaDome`** 由以下软件包组成:
### [Core](https://github.com/lavamoat/lavadome/blob/main/packages/core)
实现基本的 API 层,负责协调使用方与受保护的隔离组件之间的通信。该 API 力求在允许外部尽可能多地操作隔离组件的同时,不向任何人(甚至 LavaDome 的使用方)提供其中的实际 DOM 节点,以维持尽可能高的安全级别。
此外,它负责实现所有必要的安全强化,使 `ShadowDom` 功能的使用真正安全,这与它默认并非安全特性的原生性质形成对比(参见[安全](#Security))。
> 请记住:核心软件包不可用于生产环境!
### [JavaScript](https://github.com/lavamoat/lavadome/blob/main/packages/javascript) / [React](https://github.com/lavamoat/lavadome/blob/main/packages/react) 等
导出功能,供开发者按自己的偏好使用 **`LavaDome`**,无论是通过 JavaScript 还是作为 React 组件(或任何其他平台——[尽管提出需求!](https://github.com/lavamoat/lavadome/issues/new?title=**`LavaDome`**+misses+support+for+...))
> 注意:为框架提供 **`LavaDome`** 支持会集成我们无法控制的第三方代码,从而产生“安全盲点”。
> 请阅读 [安全](#Security) 部分,了解当 **`LavaDome`** 与第三方框架一起使用时如何尽可能保持安全。
## 安全
如果你计划在项目中使用 **`LavaDome`**,以下是需要注意的安全方面:
### `ShadowDom` 与 `iframe`
再说一次,这仍然是一个实验性项目,但我们对这个决定确实进行过深思熟虑。使用 `ShadowDom` 的一个自然替代方案是利用跨源 `iframe`。渗透跨源 `iframe` 是不可能的,并且它被 W3C 规范认定为安全关键机制。这意味着,如果意外发生漏洞,它将被视为安全漏洞,并由浏览器厂商紧急修复。
然而,这种方法的一个缺点是,集成基于 iframe 的解决方案在 UI/UX/DX 方面要困难得多,尤其是作为一款旨在大规模采用的工具。
**`LavaDome`** 需要提供顺畅自然的开发者体验,同时促进封装的 shadow DOM 节点在宿主 DOM 树中的安全集成,而 `ShadowDom` 正是为此目的构建的面向 DOM 的 API。这使得它更适合我们的目标。
虽然 `ShadowDom` API 并未被其创建者正式认可为安全工具,但它的实现非常安全,并且除极少数特定场景外,不会从 shadow DOM 树内部泄漏任何封装信息。
我们相信,通过仔细处理这些特定场景,`ShadowDom` 可以被增强为一个安全的 DOM 封装 API(值得一试)。
### 威胁
应对像 `LavaDome` 这样基于 `ShadowDom` 的解决方案当前存在的安全威胁非常重要。
#### 1. 注入
开发者可能向 **`LavaDome`** 提供 HTML/JS/CSS 内容,这些内容在加载时可能意外或故意泄漏 `ShadowDom` 内部的 DOM 节点,例如在运行时动态添加 JavaScript 代码。
阅读 [@arxenix](https://github.com/arxenix) 的[研究](https://blog.ankursundara.com/shadow-dom/#contenteditable-or-css-injection) 以了解更多关于这种技术的信息。
为防止这种可能性,**`LavaDome`** 完全不接受 DOM 节点进入 shadow DOM 树,仅支持封装纯文本。这使我们不必处理信任用户提供的 HTML/JS/CSS 内容所固有的安全问题。
我们希望在未来重新审视这一决定,因为我们正在研究一种稳定且安全的方式来支持 DOM 节点和子树输入。
#### 2. 可查找性
[find()](https://developer.mozilla.org/en-US/docs/Web/API/Window/find) API 允许开发者通过搜索节点所包含的文本来查找和提取 DOM 节点。这是迄今为止已知的唯一能够成功从 `ShadowDom` 内部泄漏 DOM 节点的 API。
<details>
<summary>
在 Firefox 中,找到文本后,可以使用 <code>getSelection()</code> API 从 `ShadowDom` 内部泄漏 DOM 节点,从而使整个构想失效:<i>(点击展开)</i>
</summary>```js
// defender
const secret = 'AN UNPREDICTABLE SECRET';
const opts = { mode:'closed' };
const root = document.body.firstElementChild.firstElementChild;
const p = document.createElement('p');
const shadow = root.attachShadow(opts);
shadow.append(p);
p.innerText = 'Secret is: ' + secret;
// attacker
setTimeout(() => {
find('Secret is:'); // assuming the Shadow includes predictable text
console.log('stolen secret: ', getSelection().anchorNode.textContent);
});

为了防御此攻击,LavaDome 的使用方绝不能将可预测的内容传递给 LavaDome API。虽然这听起来显而易见,但开发者很容易会向 LavaDome 传入类似 The secret is: ldsjf9304rjdkn 的输入,这将完全破坏 LavaDome 的安全性。尽管 ldsjf9304rjdkn 这部分无法猜测,但固定短语 "The secret is: " 仍可能被利用来揭示机密信息,尤其是当该短语此前已在 DOM 中暴露过时。
因此,在使用 LavaDome 时,开发者必须只将 100% 不可预测的文本作为输入传入。
document.execCommand('insertHTML', ...) 在 `ShadowDom` 的内部作用域中实现任意代码执行,并借此访问被封装的 DOM 节点。 (点击展开)
// attacker setTimeout(() => { console.log(1, 'stolen secret:'); const bypass = '<audio/src/onerror=console.log(2,this.nextSibling.innerHTML)>'; find('Secret is:'); // assuming the Shadow includes predictable text // assuming the found node is contenteditable=true document.execCommand('insertHTML', false, bypass); });
<div align="center"><img width="800" src="https://assets.kitploit.com/production/public/readmes/48843/b3f770ae9d6ea6070e8ed984e33a2f4df5112128d21d3c01000dd66ee1143733.png" alt="`ShadowDom` 绕过 Chromium"/></div>
</details>
为了防御这种攻击向量,**`LavaDome`** 使用可能的最高优先级样式属性(`-webkit-user-modify: unset;`)从其自定义元素中移除所有样式属性。这确保其元素不易受到恶意外部 CSS 注入的影响,这种注入会应用 `-webkit-user-modify:read-write` 属性,从而使 `ShadowDom` 元素变为 `contenteditable`。
第二种使用 `contenteditable` 作为属性的技术目前并不相关,因为 **`LavaDome`** 不支持接受 DOM 节点。
#### 3. 可选择性 & 秘密分割
如果 [`getSelection`](https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection) 被缓解,上述攻击向量就没有太大用处。通过使 **`LavaDome`** 中包含的文本不可选择,我们加强了针对如上所示可能注入的安全性。这在 Chromium 中效果良好,但我们正在解决 Firefox 中的一些问题。
如果攻击者设法猜中秘密的一个子集,他们就能攻破整个秘密(假设 `getSelection` 捕获了作用域节点,就像 Firefox 中那样)。这是因为搜索该子集会泄漏包含该秘密子集的文本节点,从而使攻击者能够访问整个秘密。
作为对策,**`LavaDome`** 将秘密的每个字符存储在其自己的 `ShadowDom` 中,确保秘密的某个子集被攻破不会导致其余部分也被攻破。这种保护还有一个额外的好处:秘密越长、可能包含的字符选项越多,攻击者泄漏整个秘密的难度就会呈指数级增加。
仍然可能发生泄露,但前提是攻击者一个接一个地暴力破解所有可能的字符,泄漏他们找到的所有 shadow,然后同步地将所有 shadow 正确重新排序,以对齐它们在 **`LavaDome`** 主宿主中的各自位置。
#### 4. 侧信道攻击
另一个众所周知的攻击是利用可继承的 CSS 属性(例如 `@font-face`)逐一将 ShadowDOM 的内容泄漏到远程服务器。
考虑一下 [@masatokinugawa](https://github.com/masatokinugawa) 记录的以下攻击 [研究](https://mksben.l0.cm/2015/10/css-based-attack-abusing-unicode-range.html)。
为了解决这个问题,LavaDome 会在父 Shadow 中添加所有可能的字符,这样此类泄漏尝试在查找所有可能的字符时会被混淆,从而使这种攻击失效(参见 https://github.com/LavaMoat/LavaDome/issues/16)。
当然,侧信道攻击有多种形式,有些更难解决,例如 [@securityMB](https://github.com/securityMB) 的 [研究](https://research.securitum.com/stealing-data-in-great-style-how-to-use-css-to-attack-web-application/),他使用了连字字体(由 [@masatokinugawa](https://github.com/masatokinugawa) 在 https://github.com/LavaMoat/LavaDome/issues/40 利用)。
为了解决这个问题,采用 LavaDome 的开发者需要制定严格的 `font-src` CSP 策略,以确保不可能通过字体向远程不可控服务器泄漏信息。
值得注意的是,这在 Safari 中(理论上)不会有用,因为在 Safari 中,这种攻击可以通过使用本地 SVG 构成字体来实施,从而使攻击者可以不受 CSP 的限制(参见 WIP @ https://github.com/LavaMoat/LavaDome/issues/40#issuecomment-2090318009)。
另一个侧信道攻击的好例子——这次不使用字体——是利用文本片段(参见 [@masatokinugawa](https://github.com/masatokinugawa) 的 [利用](https://github.com/LavaMoat/LavaDome/issues/35))。
#### 5. 防御性编码
安全的解决方案需要防御性编码实践。
- 为此,我们使用的所有原生 API 都被缓存以供内部使用,以防止攻击者重新配置全局 API 来破坏 **`LavaDome`** 的执行流程。
- 如果您在源代码中观察到非常规的样式选择,这很可能是由防御性编码原则所决定的。
- 在加载任何您不信任的脚本之前,**至关重要的是**先将 **`LavaDome`** 包含在应用中,最好是在所有脚本之前!
- 当使用 **`LavaDome`** 的框架版本时,您应该假设这些框架并非以防御性方式编写,并且所使用的原生 API 无法免受恶意干扰。请注意,外部代码的安全性不在 **`LavaDome`** 的控制范围之内。
因此,我们建议始终将此类安全解决方案与 [@agoric](https://github.com/agoric) 开发的 [SES](https://github.com/endojs/endo/tree/master/packages/ses#ses) 技术集成。这是在 [LavaMoat](https://github.com/lavamoat/lavamoat) 和 [MetaMask](https://github.com/MetaMask/metamask-extension) 遵循的安全实践。
#### 6. React 内部处理泄漏
另一件需要担心的事情(特别是在 React 的上下文中)是,提供给 React 组件的输入会被 React 主动泄漏到全局对象,从而使其可以被应用中运行的不可信实体获取(这完全破坏了 `LavaDome` 的目标)。
请参阅 [naugtur](https://github.com/naugtur) 的 [发现](https://github.com/LavaMoat/LavaDome/pull/23#issue-2093459897) 以了解更多信息。
为了平衡我们支持 React 的意图与我们不能将秘密托付给 React 的事实,`LavaDomeReact` 包导出了一些极简(但安全)的功能,可以在将秘密传给 React 之前将其与一个特殊令牌交换,而唯一能将令牌换回秘密的实体只有 `LavaDome` 本身。
虽然这很强大,但不幸的是,这要求 React 用户必须在将秘密传给 `LavaDomeReact` 之前主动执行交换。
如果用户收到的是任何非众所周知的令牌,则会抛出 `LavaDome` 生成的异常,以强制开发者安全地使用 `LavaDomeReact`。
## 免责声明
如果您阅读了以上所有内容,您应该能很好地理解为什么 **`LavaDome`** 仍然非常实验性。让非安全功能变得安全本身就存在风险,但由于这个问题领域没有现成的良好解决方案,我们认为这次尝试代表了朝着正确方向迈出的一步。
我们仍然建议使用 **`LavaDome`**,因为它相较于仅依赖当前 Web 标准来说,代表了一种明确的改进。请记住,我们的解决方案会让您的代码"更安全",但不会"绝对安全"。
此外,请记住:LavaDome 帮助您将秘密安全地带到 DOM 中。秘密在传给 LavaDome 之前是否已被泄露,不在 LavaDome 的职责范围内。
这意味着确保秘密在与 LavaDome 共享之前是安全的是您的责任。
实现这一目标的最佳方式是使用 [SES](https://github.com/endojs/endo/tree/master/packages/ses#ses) / [LavaMoat](https://github.com/lavamoat/lavamoat) 在锁定环境中运行。
--domain zoom.myorg.com - 指向 Zoom 仿冒域名的域名