From 40796933e9f444588b317c1c38a1034b9fb13699 Mon Sep 17 00:00:00 2001 From: weizman Date: Wed, 29 May 2024 01:25:01 +0300 Subject: [PATCH 1/6] poc --- packages/core/src/element.mjs | 6 +++++- packages/core/src/lavadome.mjs | 12 +++++++++++- packages/core/src/native.mjs | 2 ++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/core/src/element.mjs b/packages/core/src/element.mjs index 91f5be3..49ce1b0 100644 --- a/packages/core/src/element.mjs +++ b/packages/core/src/element.mjs @@ -51,4 +51,8 @@ export const distraction = invoker(creator({ 'top': '-10px', 'right': '-10px', 'position': 'fixed', // font-size smaller than 1px fails to be a distraction on Firefox 'font-size': '1px', -}, () => 'span', all)); \ No newline at end of file +}, () => 'span', all)); + +export const loadable = invoker(creator({ + 'display': 'none', +}, () => 'iframe')); \ No newline at end of file diff --git a/packages/core/src/lavadome.mjs b/packages/core/src/lavadome.mjs index 14a6b09..c1f2888 100644 --- a/packages/core/src/lavadome.mjs +++ b/packages/core/src/lavadome.mjs @@ -9,8 +9,10 @@ import { appendChild, replaceChildren, textContentSet, + addEventListener, + ownerDocument, } from './native.mjs'; -import {distraction, unselectable} from './element.mjs'; +import {distraction, loadable, unselectable} from './element.mjs'; import {getShadow} from './shadow.mjs'; export function LavaDome(host, opts) { @@ -23,6 +25,11 @@ export function LavaDome(host, opts) { const shadow = getShadow(host, opts); replaceChildren(shadow); + // fire everytime instance is reloaded and bail if occurs under non-top documents + const ifr = loadable(); + addEventListener(ifr, 'load', () => + ownerDocument(ifr) !== document && replaceChildren(shadow)); + // child of the shadow, where the secret is set, must be unselectable const child = unselectable(); appendChild(shadow, child); @@ -40,6 +47,9 @@ export function LavaDome(host, opts) { return textContentSet(child, text); } + // attach loadable only once per instance to avoid excessive load firing + appendChild(shadow, ifr); + // place each char of the secret in its own LavaDome protection instance map(from(text), char => { const span = createElement(document, 'span'); diff --git a/packages/core/src/native.mjs b/packages/core/src/native.mjs index 291345a..76d0ff6 100644 --- a/packages/core/src/native.mjs +++ b/packages/core/src/native.mjs @@ -19,6 +19,8 @@ const { stringify } = JSON; const n = (obj, prop, accessor) => obj && Function.prototype.call.bind(getOwnPropertyDescriptor(obj, prop)[accessor]); +export const ownerDocument = n(globalThis?.Node?.prototype, 'ownerDocument', 'get'); +export const addEventListener = n(globalThis?.EventTarget?.prototype, 'addEventListener', 'value'); export const replaceChildren = n(globalThis?.DocumentFragment?.prototype, 'replaceChildren', 'value'); export const attachShadow = n(globalThis?.Element?.prototype, 'attachShadow', 'value'); export const createElement = n(globalThis?.Document?.prototype, 'createElement', 'value'); From d1e81770ddbdaf53a5880db33dd9365de8b476c4 Mon Sep 17 00:00:00 2001 From: weizman Date: Wed, 10 Jul 2024 19:37:22 +0300 Subject: [PATCH 2/6] introduce a warning to console --- packages/core/src/lavadome.mjs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/core/src/lavadome.mjs b/packages/core/src/lavadome.mjs index c1f2888..85b118f 100644 --- a/packages/core/src/lavadome.mjs +++ b/packages/core/src/lavadome.mjs @@ -27,8 +27,16 @@ export function LavaDome(host, opts) { // fire everytime instance is reloaded and bail if occurs under non-top documents const ifr = loadable(); - addEventListener(ifr, 'load', () => - ownerDocument(ifr) !== document && replaceChildren(shadow)); + addEventListener(ifr, 'load', () => { + const ownerDoc = ownerDocument(ifr); + if (ownerDoc !== document) { + replaceChildren(shadow); + console.warn('LavaDome:', + `The document to which LavaDome was originally introduced: `, document, + `must be the same as the one this instance is inserted to: `, ownerDoc, + ); + } + }); // child of the shadow, where the secret is set, must be unselectable const child = unselectable(); From af3476fdaca05499c11f644cb6ae50d124b28fb7 Mon Sep 17 00:00:00 2001 From: weizman Date: Wed, 10 Jul 2024 19:41:19 +0300 Subject: [PATCH 3/6] error instead of warn --- packages/core/src/lavadome.mjs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/core/src/lavadome.mjs b/packages/core/src/lavadome.mjs index 85b118f..e36c472 100644 --- a/packages/core/src/lavadome.mjs +++ b/packages/core/src/lavadome.mjs @@ -31,10 +31,9 @@ export function LavaDome(host, opts) { const ownerDoc = ownerDocument(ifr); if (ownerDoc !== document) { replaceChildren(shadow); - console.warn('LavaDome:', - `The document to which LavaDome was originally introduced: `, document, - `must be the same as the one this instance is inserted to: `, ownerDoc, - ); + throw new Error(`LavaDomeCore: ` + + `The document to which LavaDome was originally introduced ` + + `must be the same as the one this instance is inserted to`); } }); From cebd9cbb3d41e9dbb457b90a7ba1401726e9091d Mon Sep 17 00:00:00 2001 From: weizman Date: Wed, 24 Apr 2024 02:42:27 +0300 Subject: [PATCH 4/6] address navigation bypass --- packages/core/src/lavadome.mjs | 14 ++++++++++++++ packages/core/src/native.mjs | 9 ++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/core/src/lavadome.mjs b/packages/core/src/lavadome.mjs index 14a6b09..3f82736 100644 --- a/packages/core/src/lavadome.mjs +++ b/packages/core/src/lavadome.mjs @@ -9,10 +9,24 @@ import { appendChild, replaceChildren, textContentSet, + navigation, + url, destination, includes, + preventDefault, stopPropagation, } from './native.mjs'; import {distraction, unselectable} from './element.mjs'; import {getShadow} from './shadow.mjs'; +// text-fragments links can be abused to leak shadow internals - block in-app redirection to them +navigation.addEventListener('navigate', event => { + const dest = url(destination(event)); + if (includes(dest, ':~:')) { + preventDefault(event); + stopPropagation(event); + throw new Error( + `LavaDomeCore: in-app redirection to text-fragments links is blocked to ensure security`); + } +}); + export function LavaDome(host, opts) { opts = options(opts); diff --git a/packages/core/src/native.mjs b/packages/core/src/native.mjs index 291345a..6945615 100644 --- a/packages/core/src/native.mjs +++ b/packages/core/src/native.mjs @@ -5,6 +5,7 @@ const { Function, Math, parseInt, WeakMap, Error, JSON, + navigation, } = globalThis; const { defineProperties, assign, @@ -26,13 +27,18 @@ export const appendChild = n(globalThis?.Node?.prototype, 'appendChild', 'value' export const textContentSet = n(globalThis?.Node?.prototype, 'textContent', 'set'); export const setAttribute = n(globalThis?.Element?.prototype, 'setAttribute', 'value'); export const toUpperCase = n(globalThis?.String?.prototype, 'toUpperCase', 'value'); +export const includes = n(globalThis?.String?.prototype, 'includes', 'value'); export const map = n(globalThis?.Array?.prototype, 'map', 'value'); export const join = n(globalThis?.Array?.prototype, 'join', 'value'); export const keys = n(globalThis?.Array?.prototype, 'keys', 'value'); export const at = n(globalThis?.Array?.prototype, 'at', 'value'); export const get = n(globalThis?.WeakMap?.prototype, 'get', 'value'); export const set = n(globalThis?.WeakMap?.prototype, 'set', 'value'); -export const toFixed = n(globalThis?.Number?.prototype, 'toFixed', 'value') +export const toFixed = n(globalThis?.Number?.prototype, 'toFixed', 'value'); +export const destination = n(globalThis?.NavigateEvent?.prototype, 'destination', 'get'); +export const url = n(globalThis?.NavigationDestination?.prototype, 'url', 'get'); +export const preventDefault = n(globalThis?.Event?.prototype, 'preventDefault', 'value'); +export const stopPropagation = n(globalThis?.Event?.prototype, 'stopPropagation', 'value'); export { // window @@ -40,6 +46,7 @@ export { Function, Math, parseInt, WeakMap, Error, JSON, + navigation, // Object defineProperties, assign, getOwnPropertyDescriptor, From 5e089144920322b4d4c309261275021909d7ab0c Mon Sep 17 00:00:00 2001 From: weizman Date: Wed, 11 Sep 2024 17:28:56 +0300 Subject: [PATCH 5/6] Update packages/core/src/lavadome.mjs Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com> --- packages/core/src/lavadome.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/lavadome.mjs b/packages/core/src/lavadome.mjs index 9581bb5..010cf30 100644 --- a/packages/core/src/lavadome.mjs +++ b/packages/core/src/lavadome.mjs @@ -39,7 +39,7 @@ export function LavaDome(host, opts) { const shadow = getShadow(host, opts); replaceChildren(shadow); - // fire everytime instance is reloaded and bail if occurs under non-top documents + // fire every time instance is reloaded and abort loading for non-top documents const ifr = loadable(); addEventListener(ifr, 'load', () => { const ownerDoc = ownerDocument(ifr); From 74684258bfe82615dc4df5b89118883e42d04392 Mon Sep 17 00:00:00 2001 From: weizman Date: Wed, 11 Sep 2024 17:30:11 +0300 Subject: [PATCH 6/6] ifr to iframe --- packages/core/src/lavadome.mjs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/src/lavadome.mjs b/packages/core/src/lavadome.mjs index 010cf30..4a0cec0 100644 --- a/packages/core/src/lavadome.mjs +++ b/packages/core/src/lavadome.mjs @@ -40,9 +40,9 @@ export function LavaDome(host, opts) { replaceChildren(shadow); // fire every time instance is reloaded and abort loading for non-top documents - const ifr = loadable(); - addEventListener(ifr, 'load', () => { - const ownerDoc = ownerDocument(ifr); + const iframe = loadable(); + addEventListener(iframe, 'load', () => { + const ownerDoc = ownerDocument(iframe); if (ownerDoc !== document) { replaceChildren(shadow); throw new Error(`LavaDomeCore: ` + @@ -69,7 +69,7 @@ export function LavaDome(host, opts) { } // attach loadable only once per instance to avoid excessive load firing - appendChild(shadow, ifr); + appendChild(shadow, iframe); // place each char of the secret in its own LavaDome protection instance map(from(text), char => { @@ -83,4 +83,4 @@ export function LavaDome(host, opts) { // add a distraction against side channel leaks attack attempts appendChild(child, distraction()); } -} \ No newline at end of file +}