diff --git a/CHANGELOG.md b/CHANGELOG.md index d440b7e..cc54e84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## next + +- Reworked sandbox init: + - UI is loading into a [sandboxed](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox) frame with `allow-scripts allow-forms allow-popups allow-modals` features enabled. That prevents access to [data storage/cookies](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy#cross-origin_data_storage_access) and some JavaScript APIs from UI scripts (because `allow-same-origin` is not enabled) but puts UI scripts in the same conditions across environments (e.g. a regular page, a page in "incognito mode", a devtools page etc). + - Added `sandboxSrc` option for `createSandbox()` to specify a sandbox page URL, needed to define a specific origin e.g. in devtools + - Added `rempl/sanbox-init` endpoint which exposes a code to inject into a sandbox page to init UI scripts, e.g. + ```html + + + ``` + ## 1.0.0-alpha.22 (June 29, 2022) - Fixed crash on UI init in a sandbox when a bundle declares variables with a name as a readonly globals referring to a window object like `top`, `parent` etc. diff --git a/package.json b/package.json index b8cbd34..2edee20 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,11 @@ "import": "./lib/browser.js" } }, + "./sandbox-init": { + "types": "./lib/sandbox/browser/sandbox-init.d.ts", + "require": "./lib/sandbox/browser/sandbox-init.cjs", + "import": "./lib/sandbox/browser/sandbox-init.js" + }, "./dist/*": "./dist/*.js", "./package.json": "./package.json" }, diff --git a/scripts/transpile.cjs b/scripts/transpile.cjs index 338c835..0a11c0c 100644 --- a/scripts/transpile.cjs +++ b/scripts/transpile.cjs @@ -193,7 +193,7 @@ async function transpileAll(options) { const { watch = false, types = false, bundle = false } = options || {}; await transpile({ - entryPoints: ['src/node.ts', 'src/browser.ts'], + entryPoints: ['src/node.ts', 'src/browser.ts', 'src/sandbox/browser/sandbox-init.ts'], outputDir: './lib', format: 'esm', watch, diff --git a/src/sandbox/browser/index.ts b/src/sandbox/browser/index.ts index c0c60ef..762e591 100644 --- a/src/sandbox/browser/index.ts +++ b/src/sandbox/browser/index.ts @@ -2,6 +2,7 @@ import { Sandbox } from '../../types.js'; import { OnInitCallback, EventTransport } from '../../transport/event.js'; import { globalThis, parent, genUID } from '../../utils/index.js'; +import { initSandboxScript } from './sandbox-init.js'; type Global = typeof globalThis; type SandboxWindow = Window | Global; @@ -9,6 +10,7 @@ type Settings = | { type: 'script'; content: Record; + sandboxSrc?: string; window?: Window; container?: HTMLElement; } @@ -23,23 +25,29 @@ const initEnvSubscriberMessage = new WeakMap(); // TODO: make tree-shaking friendly if (parent !== globalThis) { - addEventListener('message', function (event: MessageEvent) { - const data = event.data || {}; + addEventListener( + 'message', + function (event: MessageEvent) { + const data = event.data || {}; - if (event.source && data.to === 'rempl-env-publisher:connect') { - initEnvSubscriberMessage.set(event.source, data); - } - }); + if (event.source && data.to === 'rempl-env-publisher:connect') { + initEnvSubscriberMessage.set(event.source, data); + } + }, + true + ); } export function createSandbox(settings: Settings, callback: OnInitCallback) { function initSandbox(sandboxWindow: SandboxWindow) { if (settings.type === 'script') { - for (const [sourceURL, source] of Object.entries(settings.content)) { - (sandboxWindow as Global).eval( - `(function(){${source}})()\n//# sourceURL=${sourceURL}` - ); - } + sandboxWindow.postMessage( + { + action: 'rempl-sandbox-init-scripts', + scripts: settings.content, + }, + '*' + ); } if (parent !== globalThis && sandboxWindow !== globalThis) { @@ -60,7 +68,7 @@ export function createSandbox(settings: Settings, callback: OnInitCallback) { case 'rempl-env-subscriber:connect': case toSandbox: toEnv = data.from; - sandboxWindow.postMessage(data); + sandboxWindow.postMessage(data, '*'); break; case 'rempl-env-publisher:connect': @@ -108,11 +116,20 @@ export function createSandbox(settings: Settings, callback: OnInitCallback) { iframe = document.createElement('iframe'); iframe.name = genUID(); // to avoid cache iframe.onload = () => iframe?.contentWindow && initSandbox(iframe.contentWindow); + iframe.setAttribute('sandbox', 'allow-scripts allow-forms allow-popups allow-modals'); if (settings.type === 'url') { iframe.src = settings.content; + } else if (settings.sandboxSrc) { + iframe.src = settings.sandboxSrc; } else { - iframe.srcdoc = ''; + iframe.srcdoc = ''; + // iframe.src = URL.createObjectURL( + // new Blob( + // [''], + // { type: 'text/html' } + // ) + // ); } (settings.container || document.documentElement).appendChild(iframe); diff --git a/src/sandbox/browser/sandbox-init.ts b/src/sandbox/browser/sandbox-init.ts new file mode 100644 index 0000000..a88017d --- /dev/null +++ b/src/sandbox/browser/sandbox-init.ts @@ -0,0 +1,21 @@ +type SandboxInitEvent = MessageEvent<{ + action: 'rempl-sandbox-init-scripts'; + scripts: Record; +}>; + +export function initSandboxScript() { + addEventListener('message', function handleMessage(event: SandboxInitEvent) { + const { action, scripts } = event.data || {}; + + if (action === 'rempl-sandbox-init-scripts' && scripts) { + // handle message only once + removeEventListener('message', handleMessage); + + // evaluate scripts + for (const [sourceURL, source] of Object.entries(scripts)) { + // indirect eval, see detail: https://esbuild.github.io/content-types/#direct-eval + Function(`${source}\n//# sourceURL=${sourceURL}`)(); + } + } + }); +} diff --git a/src/transport/event.ts b/src/transport/event.ts index 3cfae40..0af39ba 100644 --- a/src/transport/event.ts +++ b/src/transport/event.ts @@ -308,7 +308,7 @@ export class EventTransport { payload, }; - this.realm.postMessage(message); + this.realm.postMessage(message, '*'); } }