diff --git a/packages/core/src/element.mjs b/packages/core/src/element.mjs index 092dd7c..4e45e18 100644 --- a/packages/core/src/element.mjs +++ b/packages/core/src/element.mjs @@ -53,4 +53,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 4390f68..781e1b6 100644 --- a/packages/core/src/lavadome.mjs +++ b/packages/core/src/lavadome.mjs @@ -9,11 +9,13 @@ import { appendChild, replaceChildren, textContentSet, + addEventListener, + ownerDocument, navigation, url, destination, includes, preventDefault, stopPropagation, } from './native.mjs'; -import {distraction, hardened} from './element.mjs'; +import {distraction, loadable, hardened} from './element.mjs'; import {getShadow} from './shadow.mjs'; // text-fragments links can be abused to leak shadow internals - block in-app redirection to them @@ -37,6 +39,18 @@ export function LavaDome(host, opts) { const shadow = getShadow(host, opts); replaceChildren(shadow); + // fire every time instance is reloaded and abort loading for non-top documents + const iframe = loadable(); + addEventListener(iframe, 'load', () => { + const ownerDoc = ownerDocument(iframe); + if (ownerDoc !== document) { + replaceChildren(shadow); + throw new Error(`LavaDomeCore: ` + + `The document to which LavaDome was originally introduced ` + + `must be the same as the one this instance is inserted to`); + } + }); + // child of the shadow, where the secret is set, must be hardened const child = hardened(); appendChild(shadow, child); @@ -54,6 +68,9 @@ export function LavaDome(host, opts) { return textContentSet(child, text); } + // attach loadable only once per instance to avoid excessive load firing + appendChild(shadow, iframe); + // place each char of the secret in its own LavaDome protection instance map(from(text), char => { const span = createElement(document, 'span'); @@ -66,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 +} diff --git a/packages/core/src/native.mjs b/packages/core/src/native.mjs index 6945615..9f5e8c5 100644 --- a/packages/core/src/native.mjs +++ b/packages/core/src/native.mjs @@ -20,6 +20,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');