Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect document adoption without listeners #57

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions packages/core/src/element.mjs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
'use strict';

import {
appendChild,
Array,
createElement,
entries, from,
join, keys, map,
parseInt, random,
setAttribute,
textContentSet,
toFixed,
toFixed, define,
HTMLElement,
customElements,
} from './native.mjs';
import {chars} from './char.mjs';

Expand Down Expand Up @@ -55,6 +58,14 @@ export const distraction = invoker(creator({
'font-size': '1px',
}, () => 'span', all));

export const loadable = invoker(creator({
'display': 'none',
}, () => 'iframe'));
//
export const loadable = function(cb) {
class Loadable extends HTMLElement {
constructor() { super() }
connectedCallback() { cb(element) }
}
const tag = rand(10) + '-' + rand(10);
define(customElements, tag, Loadable);
const element = createElement(document, tag);
return parent => appendChild(parent, element);
}
26 changes: 18 additions & 8 deletions packages/core/src/lavadome.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@

import {OPTIONS, options} from './options.mjs';
import {
Error, map, at,
Error, map, at, push,
defineProperties,
from, stringify,
createElement,
appendChild,
replaceChildren,
textContentSet,
addEventListener,
ownerDocument,
navigation,
navigation, Array,
url, destination, includes,
preventDefault, stopPropagation,
} from './native.mjs';
import {distraction, loadable, hardened} from './element.mjs';
import {getShadow} from './shadow.mjs';

const teardowns = Array();
const teardownAll = () => map(teardowns, t => t());

// 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));
Expand All @@ -27,8 +29,11 @@ navigation?.addEventListener('navigate', event => {
throw new Error(
`LavaDomeCore: in-app redirection to text-fragments links is blocked to ensure security`);
}
teardownAll();
});

addEventListener('pagehide', teardownAll);

export function LavaDome(host, opts) {
opts = options(opts);

Expand All @@ -39,12 +44,14 @@ export function LavaDome(host, opts) {
const shadow = getShadow(host, opts);
replaceChildren(shadow);

// LavaDome teardown invoker
const teardown = () => 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);
const attach = loadable(element => {
const ownerDoc = ownerDocument(element);
if (ownerDoc !== document) {
replaceChildren(shadow);
teardown();
throw new Error(`LavaDomeCore: ` +
`The document to which LavaDome was originally introduced ` +
`must be the same as the one this instance is inserted to`);
Expand All @@ -69,7 +76,10 @@ export function LavaDome(host, opts) {
}

// attach loadable only once per instance to avoid excessive load firing
appendChild(shadow, iframe);
attach(shadow);

// add to list of future teardowns
push(teardowns, teardown);

// place each char of the secret in its own LavaDome protection instance
map(from(text), char => {
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/native.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const {
parseInt, WeakMap,
Error, JSON,
navigation,
HTMLElement,
customElements,
} = globalThis;
const {
defineProperties, assign,
Expand All @@ -21,7 +23,6 @@ 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');
Expand All @@ -34,13 +35,15 @@ 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 push = n(globalThis?.Array?.prototype, 'push', '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 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 const define = n(globalThis?.CustomElementRegistry?.prototype, 'define', 'value');

export {
// window
Expand All @@ -49,6 +52,8 @@ export {
parseInt, WeakMap,
Error, JSON,
navigation,
HTMLElement,
customElements,
// Object
defineProperties, assign,
getOwnPropertyDescriptor,
Expand Down