
Aislamiento y encapsulación seguros de árboles DOM aprovechando ShadowDOM
⚠️ EXPERIMENTAL [WIP] - ÚSALO BAJO TU PROPIO RIESGO (más información)
Prueba LavaDome: visita la aplicación demo, abre la consola y haz todo lo que esté en tu poder para robar el secreto dentro de la instancia de LavaDome (reporta tu éxito)
Bajo los estándares web actuales, no existe una forma establecida de aislar selectivamente subárboles del DOM de manera segura. En otras palabras, no podemos controlar el acceso a secciones del DOM otorgando acceso a algunas partes mientras se lo bloqueamos a otras, si comparten el mismo entorno de ejecución de JavaScript.
Vivimos en un mundo donde ya no podemos confiar en el código de nuestras propias aplicaciones, y la ejecución del mismo origen no garantiza seguridad. Para proteger secretos en el frontend, debemos poder mostrar contenido al usuario y, al mismo tiempo, garantizar que no pueda ser comprometido por código JavaScript que se ejecute bajo el mismo origen.

Actualmente, este contenido sensible simplemente se adjunta al DOM una vez exportado, lo que lo hace totalmente accesible para todas las entidades que se ejecutan en la misma aplicación. Es decir, secciones del código que no deberían tener acceso a la clave privada podrían extraerla fácilmente en texto plano, siempre que el código malicioso tenga acceso al DOM.
Pero no te preocupes. Creemos que este es un problema solucionable 👇
LavaDome actualmente es compatible con JavaScript Vanilla y React (con más en camino)
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} />
</>;
}
Además del nodo raíz, todos los constructores aceptan un segundo argumento opcional de opciones:```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
Debido a las limitaciones del núcleo web, para integrar LavaDome de forma segura, hay algunas cosas a tener en cuenta que requieren un esfuerzo activo por parte del desarrollador que lo integra:
#### Orden de ejecución
LavaDome, como cualquier otro software de seguridad de JavaScript, siempre es vulnerable al código que se ejecuta antes que él.
Esto significa que, excepto por el código en el que confiamos absolutamente, LavaDome debe ser la primera pieza de código en cargarse en el programa de la aplicación web.
Aunque esto no significa que el desarrollador deba usarlo de inmediato (sino solo cuando lo necesite), sí debe incluir el programa lo antes posible.
Para hacerlo correctamente (de forma segura), debe ser la primera declaración import/require en todo el programa:```javascript
import '@lavamoat/lavadome-react';
import 'other-stuff';
console.log('Program starts here');
De esa manera garantizamos que LavaDome tenga tiempo de prepararse para su uso seguro.
Ten en cuenta que esto aplica de igual forma al resto de los paquetes de LavaDome y no solo a @lavamoat/lavadome-react (por lo que importar más de uno de ellos es innecesario).
Salta a Seguridad (codificación defensiva) para aprender más.
Debido a los ataques de canal lateral y las limitaciones web, importar fuentes remotas puede ser una técnica exitosa contra LavaDome. Dado que está incrustado en los reinos de CSS, abordar este problema a través de LavaDome no es posible actualmente.
Afortunadamente, esto se puede abordar de manera efectiva usando la directiva font-src de CSP.
Para mitigar esta forma de ataque, asegúrate de que tu aplicación web no permita la carga de fuentes desde servidores desconocidos.
Salta a Seguridad (canal lateral) para aprender más.
El texto proporcionado a LavaDome por el desarrollador debe ser 100% impredecible, de lo contrario puede ser atacado y filtrado.
Entonces, si tu aplicación debe presentar "your key is 234789", esto significa que tu estructura DOM debería ser:```html
your key is 234789
y no debe ser:```html
<span> <lavadome>your key is 234789</lavadome> </span>
Salta a Security(findability) para aprender más.
Integrar LavaDome podría ser complicado en el contexto de las pruebas, porque dado que LavaDome hace un buen trabajo ocultando el secreto, ¡también lo oculta bastante bien de tus pruebas!
Para integrar LavaDome con éxito en tu entorno de pruebas, puede que necesites ayuda de LavaDomeDebug, que es exportado por @lavamoat/lavadome-core:```javascript
// IMPORT/USE FOR TESTING/DEBUGGING PURPOSES ONLY - NEVER IN PRODUCTION!
import { LavaDomeDebug } from '@lavamoat/lavadome-core';
A continuación se muestran algunos de los métodos de utilidad de depuración que exporta `LavaDomeDebug` y que pueden ayudarte a probar componentes basados en `LavaDome`:
#### `getTextByRoot()`
Dado un root adjunto de `LavaDome`, `getTextByRoot()` extraerá y reconstruirá recursivamente el secreto interno. Para permitir eso, la instancia de `LavaDome` debe inicializarse originalmente con la opción UNSAFE `@unsafeOpenModeShadow`, que hace que las sombras internas de `LavaDome` sean accesibles desde el exterior.
Naturalmente, esto es UNSAFE y deja `LavaDome` totalmente vulnerable, pero tiene sentido usarlo solo con fines de prueba/depuración - asegúrate de nunca habilitar esta opción en producción.```javascript
new LavaDomeJavaScript(root, {
unsafeOpenModeShadow: isThisTestingEnv, // boolean
}).text('123456');
LavaDomeDebug.getTextByRoot(root) === '123456'; // true
stripDistractionFromText()Al usar controladores web (web drivers) para pruebas e indicarles que extraigan el texto interno de la raíz de una instancia de LavaDome, devolverán una cadena que contiene tanto el secreto como el texto de distracción de LavaDome.
El texto de distracción es importante para la seguridad (ver Seguridad (canal lateral)), pero hace que el controlador web extraiga caracteres que en realidad no forman parte del secreto.
Para solucionarlo, dado el texto obtenido por el controlador web, stripDistractionFromText() eliminará el texto de distracción, dejando solo la cadena exacta que tus pruebas esperan encontrar.
No te preocupes por el texto de distracción: nunca será visible/interactuable para el usuario en tu aplicación, pero debe existir por razones de seguridad.```javascript new LavaDomeJavaScript(root).text('123456'); const element = await driver.findElement('#ROOT'); // driver const text = await element.getText(); // driver LavaDomeDebug.stripDistractionFromText(text) === '123456'; // true
## Develop
Para configurar una compilación de desarrollo local de **`LavaDome`**, clona este repositorio y ejecuta uno de los siguientes comandos:```bash
npm install && npm install --global serve
yarn install && yarn global add serve
## Solución
La API web [`ShadowDom`](https://web.dev/articles/`ShadowDom`-v1) nos permite aislar y encapsular nodos DOM. Aunque [no está diseñada como una característica de seguridad](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` funciona bien para aislar subárboles DOM del JavaScript y CSS que se ejecutan en otras partes de la página.
El enfoque básico de **`LavaDome`** es aprovechar `ShadowDom`, abordando cuidadosamente sus [posibles brechas de seguridad](https://blog.ankursundara.com/shadow-dom/).
**[LavaDome](https://github.com/lavamoat/lavadome/)** pretende ser una **herramienta de seguridad en el conjunto de herramientas de LavaMoat** para implementar componentes solo de frontend que permitan exclusivamente interacciones con el usuario y código de confianza, mientras bloquean los intentos de acceso por parte de código JavaScript y CSS no confiable en la aplicación.
> Mención especial a [@arxenix](https://github.com/arxenix) por su [investigación](https://blog.ankursundara.com/shadow-dom/) sobre la seguridad de `ShadowDom`, que proporcionó la base para las principales mejoras de seguridad implementadas en **`LavaDome`**.
## Objetivos
El proyecto **`LavaDome`** sigue los siguientes principios fundamentales:
### Seguro
Nuestra máxima prioridad es ofrecer una seguridad hermética. Hemos envuelto la API `ShadowDom` con propiedades de seguridad avanzadas para que sea segura al presentar información sensible.
Visita [Seguridad](#Security) para obtener más información sobre este esfuerzo.
### DX
Nos esforzamos por ofrecer una experiencia de desarrollo optimizada. Para ello:
1. Ser compatible con tantos frameworks populares (React, Angular, etc.) como sea posible;
2. Hacer que la API sea fácil y sencilla de usar.
### Solo modo lectura
En esta etapa, no planeamos admitir el modo escritura, lo que significa que **`LavaDome`** solo aceptará contenido de texto plano para proteger, y nada más complejo que eso.
Esto se debe a que admitir el modo escritura requeriría implementar un DOM aislado intratable, lo que introduce múltiples complicaciones de seguridad que aún no estamos listos para afrontar, como:
1. Seguridad de los event listeners - evitar que el código externo intercepte la entrada destinada a los nodos internos de LavaDome.
2. Seguridad de la superposición - evitar que el código malicioso coloque un DOM de phishing sobre **`LavaDome`** para hacer que el usuario proporcione información sensible a la entidad equivocada.
## Diseño
La complejidad de diseño de este proyecto no es alta. Sin embargo, satisfacer los requisitos combinados de los principios de seguridad que implementa es una tarea no trivial (consulta [Seguridad](#Security)).
**`LavaDome`** consta de los siguientes paquetes:
### [Core](https://github.com/lavamoat/lavadome/blob/main/packages/core)
Implementa la capa básica de API que media la comunicación entre el consumidor y el componente aislado protegido. La API aspira a permitir tanta manipulación externa del componente aislado como sea posible sin proporcionar nodos DOM reales de su interior a nadie, ni siquiera al consumidor de LavaDome, para mantener el mayor nivel de seguridad posible.
Además, asume la responsabilidad de implementar todo el endurecimiento de seguridad necesario para que el uso de la funcionalidad `ShadowDom` sea realmente seguro, en contraste con su naturaleza nativa de no ser una característica de seguridad por defecto (consulta [Seguridad](#Security)).
> Recuerda: ¡el paquete core no debe utilizarse con fines de producción!
### [JavaScript](https://github.com/lavamoat/lavadome/blob/main/packages/javascript) / [React](https://github.com/lavamoat/lavadome/blob/main/packages/react) / etc
Exporta funcionalidades para que los desarrolladores consuman **`LavaDome`** como prefieran, ya sea mediante JavaScript o como componente de React (o cualquier otra plataforma: ¡[pregunta!](https://github.com/lavamoat/lavadome/issues/new?title=**`LavaDome`**+misses+support+for+...))
> NOTA: Ofrecer soporte de **`LavaDome`** para frameworks integra código de terceros que no controlamos, lo que genera "puntos ciegos de seguridad".
> Lee la sección [Seguridad](#Security) para aprender cómo mantenerte lo más seguro posible al usar **`LavaDome`** con frameworks de terceros.
## Seguridad
Si planeas usar **`LavaDome`** para un proyecto, estos son los aspectos de seguridad que debes tener en cuenta:
### `ShadowDom` vs `iframe`
Una vez más, este sigue siendo un proyecto experimental, pero dedicamos tiempo a esta decisión. Una alternativa natural al uso de `ShadowDom` es aprovechar los `iframe` de origen cruzado. Infiltrar un `iframe` de origen cruzado es imposible, y está reconocido como un mecanismo crítico de seguridad por la especificación W3C. Esto significa que si una brecha ocurre de algún modo, se trata como una vulnerabilidad de seguridad y los proveedores de navegadores la corrigen con urgencia.
Sin embargo, la desventaja de este enfoque es que integrar una solución basada en iframe es significativamente más difícil, en términos de UI/UX/DX, especialmente como herramienta destinada a una adopción masiva.
**`LavaDome`** necesita ofrecer una experiencia de desarrollo fluida y natural mientras facilita la integración segura de nodos encapsulados del shadow DOM dentro del árbol DOM anfitrión, y `ShadowDom` es una API orientada al DOM creada precisamente para ese propósito. Esto la hizo más adecuada para nuestros objetivos.
Si bien la API `ShadowDom` no está respaldada oficialmente como herramienta de seguridad por sus creadores, su implementación es altamente segura y no filtra ninguna información encapsulada del interior del árbol shadow DOM, excepto en escenarios muy específicos.
Creemos que abordando cuidadosamente esos mismos escenarios, `ShadowDom` puede convertirse en una API de encapsulación DOM segura (vale la pena intentarlo).
### Amenazas
Es importante abordar las amenazas de seguridad actuales que existen con soluciones basadas en `ShadowDom`, como `LavaDome`.
#### 1. Inyección
Los desarrolladores podrían proporcionar a **`LavaDome`** contenido HTML/JS/CSS que, al cargarse, pueda filtrar accidental o intencionalmente nodos DOM del interior del `ShadowDom`, por ejemplo añadiendo código JavaScript dinámicamente en tiempo de ejecución.
Lee la [investigación](https://blog.ankursundara.com/shadow-dom/#contenteditable-or-css-injection) de [@arxenix](https://github.com/arxenix) para obtener más información sobre esta técnica.
Para prevenir esta posibilidad, **`LavaDome`** no acepta nodos DOM en absoluto dentro del árbol shadow DOM, y solo admite encapsular texto plano. Esto nos permite evitar tener que lidiar con los problemas de seguridad inherentes a confiar en contenido HTML/JS/CSS proporcionado por el usuario.
Nos encantaría reconsiderar esta decisión en el futuro a medida que investiguemos un medio estable y seguro para admitir la entrada de nodos DOM y subárboles.
#### 2. Encontrabilidad
La API [find()](https://developer.mozilla.org/en-US/docs/Web/API/Window/find) permite a los desarrolladores encontrar y extraer nodos DOM buscando el texto que contienen. Esta es la única API que hasta ahora se sabe que filtra con éxito nodos DOM desde el interior de un `ShadowDom`.
<details>
<summary>
En Firefox, después de encontrar el texto, se puede usar la API <code>getSelection()</code> para filtrar nodos DOM desde el interior del `ShadowDom`, comprometiendo así toda la idea: <i>(haz clic 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);
});

Lee la investigación de @arxenix para obtener más información sobre esta técnica.
Para defenderse de este ataque, el consumidor de LavaDome no debe pasar contenido predecible a la API de LavaDome. Aunque esto pueda parecer obvio, los desarrolladores podrían verse fácilmente tentados a pasar a LavaDome una entrada similar a The secret is: ldsjf9304rjdkn, lo que comprometería por completo la seguridad de LavaDome. Aunque la parte ldsjf9304rjdkn sea imposible de adivinar, la frase fija "The secret is: " podría explotarse para revelar el secreto, especialmente si había sido expuesta previamente en el DOM.
Por lo tanto, al usar LavaDome, los desarrolladores DEBEN pasar únicamente texto 100% impredecible como entrada.
document.execCommand('insertHTML', ...) para lograr la ejecución arbitraria de código en el ámbito interno del `ShadowDom`, y utilizarlo para acceder a los nodos DOM encapsulados. (haz clic 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 defenderse de este vector de ataque, **`LavaDome`** elimina todos los atributos de estilo de sus elementos personalizados utilizando el atributo de estilo de mayor prioridad posible (`-webkit-user-modify: unset;`). Esto garantiza que sus elementos no sean vulnerables a la inyección de CSS externo malicioso que aplique el atributo `-webkit-user-modify:read-write`, lo que haría que los elementos `ShadowDom` fueran `contenteditable`.
La segunda técnica de usar `contenteditable` como atributo no es relevante actualmente, ya que **`LavaDome`** no admite la aceptación de nodos DOM.
#### 3. Seleccionabilidad y división del secreto
Los vectores de ataque anteriores no son tan útiles si se mitiga [`getSelection`](https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection). Al hacer que el texto contenido en **`LavaDome`** no sea seleccionable, endurecemos la seguridad contra posibles inyecciones como se demostró anteriormente. Esto funciona bien en Chromium, pero estamos resolviendo algunos problemas con Firefox.
Si un atacante logra adivinar un subconjunto del secreto, puede comprometer el secreto completo (asumiendo que `getSelection` captura nodos con ámbito como en Firefox). Esto se debe a que buscar el subconjunto filtrará el nodo de texto que incluye ese subconjunto del secreto, dando al atacante acceso al secreto completo.
Como contramedida, **`LavaDome`** almacena cada carácter del secreto en su propio `ShadowDom`, asegurando que comprometer un subconjunto del secreto no conduzca a que el resto también se vea comprometido. Esta salvaguarda tiene el beneficio adicional de hacer exponencialmente más difícil para los atacantes filtrar el secreto completo cuanto más largo sea y más opciones de caracteres incluya potencialmente.
Una brecha sigue siendo posible, pero solo si el atacante prueba por fuerza bruta todos los caracteres posibles uno por uno, filtra todas las sombras que encuentre y luego reordena sincrónicamente todas las sombras correctamente para alinearlas con sus respectivas posiciones dentro del host principal de **`LavaDome`**.
#### 4. Canal lateral
Otro ataque bien conocido es filtrar el contenido de los ShadowDOM utilizando propiedades CSS heredables como `@font-face` a un servidor remoto, carácter por carácter.
Considere la siguiente [investigación](https://mksben.l0.cm/2015/10/css-based-attack-abusing-unicode-range.html) de ataque documentada por [@masatokinugawa](https://github.com/masatokinugawa).
Para abordar eso, LavaDome agrega a la Shadow padre todos los caracteres posibles, de modo que dicho intento de filtración se confunda al encontrar todos los caracteres posibles, dejando este ataque inútil (ver https://github.com/LavaMoat/LavaDome/issues/16).
Por supuesto, los canales laterales se presentan de muchas formas, algunas más difíciles de abordar, como la [investigación](https://research.securitum.com/stealing-data-in-great-style-how-to-use-css-to-attack-web-application/) de [@securityMB](https://github.com/securityMB) en la que utiliza fuentes de ligadura (exploit de [@masatokinugawa](https://github.com/masatokinugawa) @ https://github.com/LavaMoat/LavaDome/issues/40).
Para abordar eso, se espera que los desarrolladores que adopten LavaDome dicten una política CSP estricta de `font-src` para asegurarse de que la filtración mediante fuentes no sea posible hacia servidores remotos incontrolables.
Cabe señalar que esto (teóricamente) no será útil en Safari, donde este ataque puede llevarse a cabo utilizando SVG local para formar las fuentes, lo que permite a los atacantes permanecer independientes de CSP (ver WIP @ https://github.com/LavaMoat/LavaDome/issues/40#issuecomment-2090318009).
Otro gran ejemplo de ataques de canal lateral - esta vez sin usar fuentes - es aprovechar los fragmentos de texto (ver [@masatokinugawa](https://github.com/masatokinugawa) [exploit](https://github.com/LavaMoat/LavaDome/issues/35)).
#### 5. Codificación defensiva
Una solución segura requiere prácticas de codificación defensiva.
- Con este fin, todas las API nativas que utilizamos se almacenan en caché para uso interno, para evitar que los atacantes reconfiguran las API globales y saboteen el flujo de ejecución de **`LavaDome`**.
- Si observa opciones de estilo poco convencionales en el código fuente, es muy probable que hayan sido motivadas por principios de codificación defensiva.
- Es **crucial** incluir **`LavaDome`** en la aplicación antes de cualquier script en el que no confíe, ¡y preferiblemente antes de TODOS los scripts!
- Cuando use las versiones de framework de **`LavaDome`**, debe asumir que estos frameworks no están escritos de forma defensiva y que las API nativas utilizadas no están a salvo de interferencias maliciosas. Tenga en cuenta que la seguridad del código externo está fuera del control de **`LavaDome`**.
Por lo tanto, recomendamos integrar siempre dichas soluciones de seguridad con la tecnología [SES](https://github.com/endojs/endo/tree/master/packages/ses#ses) desarrollada por [@agoric](https://github.com/agoric). Esta es una práctica de seguridad seguida en [LavaMoat](https://github.com/lavamoat/lavamoat) y [MetaMask](https://github.com/MetaMask/metamask-extension).
#### 6. Fugas de procesamiento interno de React
Otra cosa de la que preocuparse (específicamente en el contexto de React) es el hecho de que la entrada proporcionada a los componentes de React está siendo filtrada activamente por este al objeto global, lo que la pone a disposición de entidades no confiables que se ejecutan en la aplicación (lo que socava por completo el objetivo de `LavaDome`).
Consulte el [descubrimiento](https://github.com/LavaMoat/LavaDome/pull/23#issue-2093459897) de [naugtur](https://github.com/naugtur) para obtener más información.
Para equilibrar nuestra intención de soportar React con el hecho de que no podemos confiarle nuestro secreto, el paquete `LavaDomeReact` exporta una funcionalidad mínima (aunque segura) para intercambiar el secreto por un token especial antes de pasarlo a React, donde la única entidad que puede canjear ese token de vuelta por el secreto es el propio `LavaDome`.
Aunque es potente, lamentablemente esto requiere que los usuarios de React realicen activamente el intercambio antes de pasar el secreto a `LavaDomeReact`.
Si el usuario recibe cualquier cosa que no sea un token conocido, se lanza una excepción generada por `LavaDome`, para obligar a los desarrolladores a usar `LavaDomeReact` de forma segura.
## Descargo de responsabilidad
Si ha leído todo lo anterior, debería tener una buena idea de por qué **`LavaDome`** sigue siendo muy experimental. Hacer segura una característica que no es de seguridad es intrínsecamente arriesgado, pero como este espacio de problemas no tiene buenas soluciones existentes, creemos que este intento representa un paso en la dirección correcta.
Aún recomendamos usar **`LavaDome`**, ya que representa una mejora inequívoca en comparación con depender únicamente de los estándares web actuales. Solo recuerde que nuestra solución hará que su código sea "más seguro", pero no "seguro".
Además, recuerde: LavaDome le ayuda a llevar un secreto al DOM de forma segura. El hecho de que el secreto se haya visto comprometido o no antes de pasarse a LavaDome está fuera del alcance de LavaDome.
Esto significa que es su responsabilidad asegurarse de que el secreto esté a salvo hasta el momento en que lo comparta con LavaDome.
La mejor manera de lograrlo es ejecutándose en un entorno restringido utilizando [SES](https://github.com/endojs/endo/tree/master/packages/ses#ses) / [LavaMoat](https://github.com/lavamoat/lavamoat).