
Isolamento e encapsulamento seguros de árvores DOM usando ShadowDOM
⚠️ EXPERIMENTAL [WIP] - USE POR SUA CONTA E RISCO (saiba mais)
Tente quebrar o LavaDome - visite o aplicativo de demonstração, abra o console e faça tudo ao seu alcance para roubar o segredo de dentro da instância do LavaDome (relate seu sucesso)
Nos padrões web atuais, não há uma forma estabelecida de isolar seletivamente subárvores do DOM de maneira segura. Em outras palavras, não podemos controlar o acesso a seções do DOM concedendo acesso a algumas partes enquanto bloqueamos o acesso de outras, se elas compartilharem o mesmo ambiente de execução JavaScript.
Vivemos em um mundo onde não podemos mais confiar no código dos nossos próprios aplicativos, e a execução de mesma origem não garante segurança. Para proteger segredos no frontend, precisamos ser capazes de apresentar conteúdo ao usuário enquanto garantimos que ele não possa ser comprometido por código JavaScript executado sob a mesma origem.

Atualmente, esse conteúdo sensível é simplesmente anexado ao DOM após ser exportado, tornando-o totalmente acessível a todas as entidades que executam no mesmo aplicativo. Ou seja, seções do código que não deveriam ter acesso à chave privada poderiam extraí-la facilmente em texto simples, desde que o código malicioso tenha acesso ao DOM.
Mas fique tranquilo. Acreditamos que este é um problema solucionável 👇
O LavaDome atualmente suporta JavaScript Vanilla e React (com mais a caminho)
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/HEAD/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} />
</>;
}
Além do nó raiz, todos os construtores aceitam um 2º argumento opcional de opções:```javascript // javascript new LavaDomeJavaScript(root, { // boolean unsafeOpenModeShadow: false, });
// react function Secret({ text }) { const {token} = toLavaDomeCapabilities(text); return <LavaDomeReact token={token} // boolean unsafeOpenModeShadow={false} /> }
### Uso Seguro
Devido às limitações do núcleo web, para integrar o LavaDome com segurança, há algumas coisas a serem consideradas que exigem algum esforço ativo por parte do desenvolvedor que realiza a integração:
#### Ordem de Execução
O LavaDome, como qualquer outro software de segurança JavaScript, está sempre vulnerável a código que é executado antes dele.
Isso significa que, exceto para código em que confiamos absolutamente, o LavaDome deve ser o primeiro pedaço de código a carregar no programa da aplicação web.
Embora isso não signifique que o desenvolvedor deva usá-lo imediatamente (mas sim apenas quando necessário), ele deve, no entanto, incluir o programa o mais cedo possível.
Para fazer isso corretamente (com segurança), ele deve ser a primeira declaração de import/require em todo o programa:```javascript
import '@lavamoat/lavadome-react';
import 'other-stuff';
console.log('Program starts here');
Dessa forma, garantimos que o LavaDome se prepare para o uso seguro.
Observe que isso se aplica de forma semelhante ao restante dos pacotes do LavaDome e não apenas ao @lavamoat/lavadome-react (portanto, importar mais de um deles é desnecessário).
Vá para Security(defensive-coding) para saber mais.
Devido a ataques de side channel e limitações da web, importar fontes remotas pode ser uma técnica eficaz contra o LavaDome. Como isso está embutido no âmbito do CSS, atualmente não é possível resolver esse problema por meio do LavaDome.
Felizmente, isso pode ser resolvido de forma eficaz usando a diretiva font-src do CSP.
Para mitigar essa forma de ataque, certifique-se de que seu aplicativo web não permita buscar fontes de servidores desconhecidos.
Vá para Security(side-channeling) para saber mais.
O texto fornecido ao LavaDome pelo desenvolvedor deve ser 100% imprevisível; caso contrário, pode ser atacado e vazado.
Portanto, se o seu aplicativo precisar apresentar "your key is 234789", isso significa que sua estrutura DOM deve ser:```html
your key is 234789
e não deve ser:```html
<span> <lavadome>your key is 234789</lavadome> </span>
Vá para Segurança(findability) para saber mais.
Integrar LavaDome pode ser complicado no contexto de testá-lo, porque, como o LavaDome faz um bom trabalho ao esconder o segredo, ele o esconde muito bem dos seus testes também!
Para integrar LavaDome com sucesso no seu ambiente de testes, você pode precisar de alguma ajuda de LavaDomeDebug, que é exportado por @lavamoat/lavadome-core:```javascript
// IMPORT/USE FOR TESTING/DEBUGGING PURPOSES ONLY - NEVER IN PRODUCTION!
import { LavaDomeDebug } from '@lavamoat/lavadome-core';
Aqui estão alguns dos métodos utilitários de depuração que `LavaDomeDebug` exporta e que podem ajudar você a testar componentes baseados em `LavaDome`:
#### `getTextByRoot()`
Dado um root (`raiz`) anexado a um `LavaDome`, `getTextByRoot()` extrairá e reconstruirá recursivamente o segredo interno. Para que isso seja possível, a instância de `LavaDome` deve ser iniciada originalmente com a opção INSEGURA `@unsafeOpenModeShadow`, que torna as sombras internas de `LavaDome` acessíveis a partir de fora.
Naturalmente, isso é INSEGURO e deixa `LavaDome` totalmente vulnerável, mas faz sentido usar apenas para fins de teste/depuração - certifique-se de nunca habilitar essa opção em produção!```javascript
new LavaDomeJavaScript(root, {
unsafeOpenModeShadow: isThisTestingEnv, // boolean
}).text('123456');
LavaDomeDebug.getTextByRoot(root) === '123456'; // true
stripDistractionFromText()Ao usar web drivers para testes e instruí-los a extrair o texto interno da raiz de uma instância de LavaDome, eles retornarão uma string contendo tanto o segredo quanto o texto de distração do LavaDome.
O texto de distração é importante para a segurança (ver Security(side-channeling)), mas faz com que o web driver extraia caracteres que não fazem realmente parte do segredo.
Para resolver isso, dado o texto obtido pelo web driver, stripDistractionFromText() removerá o texto de distração, deixando apenas a string exata que seus testes esperam encontrar.
Não se preocupe com o texto de distração; ele nunca será visível/interativo para o usuário no seu aplicativo, mas deve existir por motivos de segurança.```javascript new LavaDomeJavaScript(root).text('123456'); const element = await driver.findElement('#ROOT'); // driver const text = await element.getText(); // driver LavaDomeDebug.stripDistractionFromText(text) === '123456'; // true
## Desenvolvimento
Para configurar uma compilação de desenvolvimento local do **`LavaDome`**, clone este repositório e execute um dos seguintes comandos:```bash
npm install && npm install --global serve
I don't see any content after "INPUT:" — the chunk appears to be empty. There's no text to translate.```bash yarn install && yarn global add serve
## Solução
A [`ShadowDom`](https://web.dev/articles/`ShadowDom`-v1) Web API nos permite isolar e encapsular nós do DOM. Embora ela [não tenha sido projetada como um recurso de segurança](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.), o `ShadowDom` funciona bem para isolar subárvores do DOM de JavaScript e CSS que estejam sendo executados em outras partes da página.
A abordagem básica do **`LavaDome`** é aproveitar o `ShadowDom`, ao mesmo tempo em que aborda cuidadosamente suas [possíveis lacunas de segurança](https://blog.ankursundara.com/shadow-dom/).
O **[LavaDome](https://github.com/lavamoat/lavadome/)** tem a intenção de ser uma **ferramenta de segurança na caixa de ferramentas do LavaMoat** para implementar componentes exclusivamente de frontend que permitam somente interações com o usuário e código confiável, bloqueando tentativas de acesso por código JavaScript e CSS não confiável no aplicativo.
> Um agradecimento especial ao [@arxenix](https://github.com/arxenix) pela [pesquisa](https://blog.ankursundara.com/shadow-dom/) sobre segurança do `ShadowDom`, que serviu de base para as principais melhorias de segurança implementadas no **`LavaDome`**.
## Objetivos
O projeto **`LavaDome`** segue os seguintes princípios fundamentais:
### Segurança
Nossa prioridade máxima é fornecer segurança hermética. Envolvemos a API `ShadowDom` com propriedades de segurança avançadas para torná-la segura ao apresentar informações sensíveis.
Visite [Segurança](#Security) para saber mais sobre esse esforço.
### DX
Nós nos esforçamos para proporcionar uma experiência de desenvolvimento simplificada. Para isso, vamos:
1. Suportar o maior número possível de frameworks populares (React, Angular, etc.);
2. Tornar a API fácil e simples de usar.
### Somente leitura
Nesta fase, não planejamos suportar o modo de escrita, ou seja, o **`LavaDome`** aceitará apenas conteúdo em texto simples para proteção, e nada mais complexo do que isso.
Isso porque suportar o modo de escrita exigirá a implementação de um DOM isolado intratável, o que introduz múltiplas complicações de segurança que ainda não estamos prontos para enfrentar neste momento, tais como:
1. Segurança dos listeners de eventos — impedir que códigos externos interceptem entradas destinadas aos nós internos do LavaDome.
2. Segurança de sobreposição — impedir que códigos maliciosos coloquem um DOM de phishing sobre o **`LavaDome`** para fazer o usuário fornecer informações sensíveis à entidade errada.
## Design
A complexidade de design deste projeto não é alta. No entanto, satisfazer os requisitos combinados dos princípios de segurança que ele implementa é uma tarefa não trivial (veja [Segurança](#Security)).
O **`LavaDome`** consiste nos seguintes pacotes:
### [Core](https://github.com/lavamoat/lavadome/blob/HEAD/packages/core)
Implementa a camada básica da API que medeia a comunicação entre o consumidor e o componente isolado protegido. A API busca permitir o máximo de manipulação externa do componente isolado possível, sem fornecer a ninguém os nós reais do DOM internos a ele — nem mesmo ao consumidor do LavaDome — para manter o mais alto nível de segurança possível.
Além disso, assume a responsabilidade de implementar todo o endurecimento de segurança necessário para tornar o uso do recurso `ShadowDom` verdadeiramente seguro, em contraste com sua natureza nativa de não ser um recurso de segurança por padrão (veja [Segurança](#Security)).
> Lembre-se: o pacote core não deve ser usado em produção!
### [JavaScript](https://github.com/lavamoat/lavadome/blob/HEAD/packages/javascript) / [React](https://github.com/lavamoat/lavadome/blob/HEAD/packages/react) / etc
Exportam funcionalidades para que desenvolvedores consumam o **`LavaDome`** da forma que preferirem, seja por JavaScript ou como um componente React (ou qualquer outra plataforma — [pergunte à vontade!](https://github.com/lavamoat/lavadome/issues/new?title=**`LavaDome`**+misses+support+for+...))
> NOTA: Fornecer suporte do **`LavaDome`** para frameworks integra código de terceiros que não controlamos, o que gera "pontos cegos de segurança".
> Por favor, leia a seção [Segurança](#Security) para saber como permanecer o mais seguro possível ao usar o **`LavaDome`** com frameworks de terceiros.
## Segurança
Se você planeja usar o **`LavaDome`** em um projeto, estes são os aspectos de segurança dos quais deve estar ciente:
### `ShadowDom` vs `iframe`
Novamente, este ainda é um projeto experimental, mas pensamos bastante nessa decisão. Uma alternativa natural ao uso do `ShadowDom` é aproveitar `iframe`s de origem cruzada. Infiltrar um `iframe` de origem cruzada é impossível, e ele é reconhecido como um mecanismo crítico de segurança pela especificação da W3C. Isso significa que, se uma violação acontecer de alguma forma, ela é tratada como uma vulnerabilidade de segurança e corrigida com urgência pelos fornecedores de navegadores.
A desvantagem dessa abordagem, no entanto, é que integrar uma solução baseada em iframe é significativamente mais difícil, em termos de UI/UX/DX, especialmente para uma ferramenta que busca adoção em massa.
O **`LavaDome`** precisa oferecer uma experiência de desenvolvedor fluida e natural, ao mesmo tempo em que facilita a integração segura de nós de shadow DOM encapsulados dentro da árvore DOM hospedeira, e o `ShadowDom` é uma API orientada ao DOM criada exatamente para esse propósito. Isso o tornou mais adequado aos nossos objetivos.
Embora a API `ShadowDom` não seja oficialmente endossada como uma ferramenta de segurança por seus criadores, sua implementação é altamente segura e não vaza nenhuma informação encapsulada de dentro da árvore de shadow DOM, exceto em cenários muito específicos.
Acreditamos que, ao abordar cuidadosamente exatamente esses cenários, o `ShadowDom` pode ser aprimorado para se tornar uma API segura de encapsulamento de DOM (vale a pena tentar).
### Ameaças
É importante abordar as ameaças de segurança atuais que existem em soluções baseadas em `ShadowDom`, como o `LavaDome`.
#### 1. Injeção
Os desenvolvedores podem fornecer ao **`LavaDome`** conteúdo HTML/JS/CSS que, quando carregado, pode vazar acidental ou intencionalmente nós do DOM de dentro do `ShadowDom`, por exemplo, adicionando código JavaScript dinamicamente em tempo de execução.
Leia a [pesquisa](https://blog.ankursundara.com/shadow-dom/#contenteditable-or-css-injection) do [@arxenix](https://github.com/arxenix) para saber mais sobre essa técnica.
Para evitar essa possibilidade, o **`LavaDome`** não aceita absolutamente nenhum nó do DOM na árvore de shadow DOM e suporta apenas encapsular texto simples. Isso nos permite evitar ter de lidar com os problemas de segurança inerentes à confiança em conteúdo HTML/JS/CSS fornecido pelo usuário.
Adoraríamos rever essa decisão no futuro, enquanto pesquisamos uma forma estável e segura de suportar entrada de nós do DOM e subárvores.
#### 2. Encontrabilidade
A API [find()](https://developer.mozilla.org/en-US/docs/Web/API/Window/find) permite que desenvolvedores encontrem e extraiam nós do DOM pesquisando pelo texto que eles contêm. Esta é a única API conhecida até agora que vaza com sucesso nós do DOM de dentro de um `ShadowDom`.
<details>
<summary>
No Firefox, após encontrar o texto, pode-se usar a API <code>getSelection()</code> para vazar nós do DOM de dentro do `ShadowDom`, comprometendo assim toda a ideia: <i>(clique para expandir)</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);
});

Leia a pesquisa de @arxenix para saber mais sobre essa técnica.
Para se defender contra esse ataque, o consumidor do LavaDome não deve passar conteúdo previsível para a API do LavaDome. Embora isso possa parecer óbvio, os desenvolvedores podem facilmente ser tentados a passar ao LavaDome uma entrada que se pareça com algo como The secret is: ldsjf9304rjdkn, o que comprometeria totalmente a segurança do LavaDome. Mesmo que a parte ldsjf9304rjdkn seja impossível de adivinhar, a frase fixa "The secret is: " poderia ser explorada para revelar o segredo, especialmente se ela já tiver sido exposta no DOM.
Portanto, ao usar LavaDome, os desenvolvedores DEVEM passar apenas texto 100% imprevisível como entrada.
document.execCommand('insertHTML', ...) para obter execução arbitrária de código no escopo interno do `ShadowDom` e usar isso para acessar os nós DOM encapsulados. (clique para expandir)
// 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` bypass Chromium"/></div>
</details>
Para se defender contra esse vetor de ataque, **`LavaDome`** remove todos os atributos de estilo de seus elementos personalizados usando o atributo de estilo de maior prioridade possível (`-webkit-user-modify: unset;`). Isso garante que seus elementos não estejam vulneráveis à injeção de CSS externo malicioso que aplique o atributo `-webkit-user-modify:read-write`, o que tornaria os elementos `ShadowDom` `contenteditable`.
A segunda técnica de usar `contenteditable` como atributo não é atualmente relevante, pois **`LavaDome`** não suporta aceitar nós do DOM.
#### 3. Selecionabilidade e Divisão de Segredos
Os vetores de ataque acima não são tão úteis se [`getSelection`](https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection) for mitigado. Ao tornar o texto contido em **`LavaDome`** não selecionável, endurecemos a segurança contra possíveis injeções, como demonstrado acima. Isso funciona bem no Chromium, mas estamos resolvendo alguns problemas com o Firefox.
Se um invasor conseguir adivinhar um subconjunto do segredo, ele pode comprometer o segredo inteiro (assumindo que `getSelection` capture nós com escopo, como no Firefox). Isso ocorre porque a busca pelo subconjunto vazará o nó de texto que contém esse subconjunto do segredo, dando ao invasor acesso ao segredo inteiro.
Como contramedida, **`LavaDome`** armazena cada caractere do segredo em seu próprio `ShadowDom`, garantindo que comprometer um subconjunto do segredo não leve ao comprometimento do restante. Essa salvaguarda tem o benefício adicional de tornar exponencialmente mais difícil para invasores vazar o segredo inteiro quanto mais longo ele for e quanto mais opções de caracteres potencialmente incluir.
Uma violação ainda é possível, mas apenas se o invasor testar por força bruta todos os caracteres possíveis, um a um, vazar todas as sombras que encontrar e, em seguida, reordenar sincronamente todas as sombras corretamente para alinhá-las às suas respectivas posições dentro do host principal do **`LavaDome`**.
#### 4. Canal lateral
Outro ataque bem conhecido é vazar o conteúdo de ShadowDOMs usando propriedades CSS herdáveis, como `@font-face`, para um servidor remoto, caractere por caractere.
Considere a seguinte [pesquisa](https://mksben.l0.cm/2015/10/css-based-attack-abusing-unicode-range.html) de ataque documentada por [@masatokinugawa](https://github.com/masatokinugawa).
Para lidar com isso, o LavaDome adiciona ao Shadow pai todos os caracteres possíveis, de modo que essa tentativa de vazamento fique confusa ao encontrar todos os caracteres possíveis, tornando esse ataque inútil (veja https://github.com/LavaMoat/LavaDome/issues/16).
É claro que o canal lateral se apresenta de muitas formas, algumas mais difíceis de abordar, como a [pesquisa](https://research.securitum.com/stealing-data-in-great-style-how-to-use-css-to-attack-web-application/) de [@securityMB](https://github.com/securityMB), na qual ele usa fontes de ligadura (exploit por [@masatokinugawa](https://github.com/masatokinugawa) @ https://github.com/LavaMoat/LavaDome/issues/40).
Para lidar com isso, espera-se que os desenvolvedores que adotam o LavaDome definam uma política CSP estrita de `font-src` para garantir que o vazamento por fontes não seja possível para servidores remotos incontroláveis.
Vale notar que isso (teoricamente) não será útil no Safari, onde esse ataque pode ser executado usando SVG local para formar as fontes, permitindo assim que os atacantes permaneçam independentes da CSP (veja WIP @ https://github.com/LavaMoat/LavaDome/issues/40#issuecomment-2090318009).
Outro ótimo exemplo para ataques de canal lateral - desta vez sem usar fontes - é aproveitar fragmentos de texto (veja [@masatokinugawa](https://github.com/masatokinugawa) [exploit](https://github.com/LavaMoat/LavaDome/issues/35)).
#### 5. Codificação defensiva
Uma solução segura exige práticas de codificação defensiva.
- Para esse fim, todas as APIs nativas que usamos são armazenadas em cache para uso interno, a fim de impedir que invasores reconfigurem APIs globais para sabotar o fluxo de execução do **`LavaDome`**.
- Se você observar escolhas estilísticas não convencionais no código-fonte, há uma boa chance de que elas tenham sido orientadas por princípios de codificação defensiva.
- É **crucial** incluir o **`LavaDome`** no aplicativo antes de qualquer script que você não confie e, de preferência, antes de TODOS os scripts!
- Ao usar as versões de framework do **`LavaDome`**, você deve presumir que esses frameworks não são escritos de forma defensiva e que as APIs nativas usadas não estão a salvo de interferência maliciosa. Esteja avisado de que a segurança do código externo está fora do controle do **`LavaDome`**.
Portanto, recomendamos sempre integrar essas soluções de segurança com a tecnologia [SES](https://github.com/endojs/endo/tree/master/packages/ses#ses) desenvolvida por [@agoric](https://github.com/agoric). Essa é uma prática de segurança adotada na [LavaMoat](https://github.com/lavamoat/lavamoat) e na [MetaMask](https://github.com/MetaMask/metamask-extension).
#### 6. Vazamento do processamento interno do React
Outra coisa com que se preocupar (especificamente no contexto do React) é o fato de que a entrada fornecida aos componentes React está sendo ativamente vazada por ele para o objeto global, tornando-a disponível para entidades não confiáveis que executam no aplicativo (o que mina completamente o objetivo do `LavaDome`).
Consulte a [descoberta](https://github.com/LavaMoat/LavaDome/pull/23#issue-2093459897) de [naugtur](https://github.com/naugtur) para saber mais.
Para equilibrar nossa intenção de suportar o React com o fato de não podermos confiar a ele nosso segredo, o pacote `LavaDomeReact` exporta algumas funcionalidades mínimas (porém seguras) para trocar o segredo por um token especial antes de passá-lo ao React, onde a única entidade que pode trocar esse token de volta pelo segredo é o próprio `LavaDome`.
Embora poderoso, isso infelizmente exige que os usuários do React realizem ativamente a troca antes de passar o segredo para o `LavaDomeReact`.
Se algo diferente de um token bem conhecido for recebido pelo usuário, uma exceção gerada por `LavaDome` é lançada, para forçar os desenvolvedores a usarem `LavaDomeReact` com segurança.
## Aviso Legal
Se você leu tudo acima, deve ter uma boa noção de por que **`LavaDome`** ainda é muito experimental. Tornar segura uma funcionalidade que não é de segurança é inerentemente arriscado, mas como esse espaço de problema não possui boas soluções existentes, sentimos que esta tentativa representa um passo na direção certa.
Ainda recomendamos usar **`LavaDome`**, pois representa uma melhoria inequívoca em comparação a depender apenas dos padrões web atuais. Apenas lembre-se de que nossa solução tornará seu código "mais seguro", mas não "seguro".
Além disso, lembre-se: o LavaDome ajuda você a levar um segredo ao DOM com segurança. Se o segredo foi ou não comprometido antes de ser passado ao LavaDome está fora do escopo do LavaDome.
Isso significa que é sua responsabilidade garantir que o segredo esteja seguro até o ponto em que você o compartilha com o LavaDome.
A melhor maneira de conseguir isso é executando em um ambiente restrito usando [SES](https://github.com/endojs/endo/tree/master/packages/ses#ses) / [LavaMoat](https://github.com/lavamoat/lavamoat).