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

feat(labs/ssr): Prevent exposure of window object on globalThis #3431

Merged
merged 7 commits into from
Dec 13, 2022
23 changes: 18 additions & 5 deletions packages/labs/ssr/src/lib/dom-shim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ import fetch from 'node-fetch';
* generally only be true when adding window to a fresh VM context that
* starts with nothing.
* @param props Additional properties to add to the window global
* @param exposeWindow Whether the `window` object should be exposed globally
steveworkman marked this conversation as resolved.
Show resolved Hide resolved
steveworkman marked this conversation as resolved.
Show resolved Hide resolved
*/
export const getWindow = ({
includeJSBuiltIns = false,
props = {},
exposeWindow = true,
}): {[key: string]: unknown} => {
const attributes: WeakMap<HTMLElement, Map<string, string>> = new WeakMap();
const attributesForElement = (element: HTMLElement) => {
Expand Down Expand Up @@ -159,7 +161,13 @@ export const getWindow = ({
...props,
};

window.window = window;
// Many libraries look for `window` to determine a browser environment
// This prevents that exposure as `globalThis` is enough in the vast majority of cases
// This is currently off by default, but may become default in future versions
// See https://github.com/lit/lit/issues/3412
if (exposeWindow) {
window.window = window;
}

if (includeJSBuiltIns) {
Object.assign(window, {
Expand Down Expand Up @@ -198,14 +206,19 @@ export const getWindow = ({
return window;
};

export const installWindowOnGlobal = (props: {[key: string]: unknown} = {}) => {
export const installWindowOnGlobal = (
props: {[key: string]: unknown} = {},
exposeWindow = true
) => {
// Avoid installing the DOM shim if one already exists
if (globalThis.window === undefined) {
const window = getWindow({props});
const window = getWindow({props, exposeWindow});
// Copy initial window globals to node global
Object.assign(globalThis, window);
// Set up global reference to window so all globals added to window are
// added to the node global
globalThis.window = globalThis as typeof globalThis & Window;
// added to the node global, unless we explicitly don't want to do this.
if (exposeWindow) {
globalThis.window = globalThis as typeof globalThis & Window;
}
}
};
59 changes: 37 additions & 22 deletions packages/labs/ssr/src/test/lib/dom-shim_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,46 @@
* SPDX-License-Identifier: BSD-3-Clause
*/

import {getWindow} from '../../lib/dom-shim.js';
import {installWindowOnGlobal, getWindow} from '../../lib/dom-shim.js';
import {test} from 'uvu';
// eslint-disable-next-line import/extensions
import * as assert from 'uvu/assert';

const window = getWindow({}) as unknown as Window;
const {HTMLElement} = window.window;
function testWithHtmlElement(HTMLElement: any) {
test('elements without an attached shadow root should expose a null value from the "shadowRoot" property', () => {
class UnattachedShadowRoot extends HTMLElement {}
const element = new UnattachedShadowRoot();
assert.is(element.shadowRoot, null);
});
test('elements defined with an open shadow root should expose it\'s shadow root from the "shadowRoot" property', () => {
class OpenShadowRoot extends HTMLElement {}
const element = new OpenShadowRoot();
const shadow = element.attachShadow({mode: 'open'});
assert.is(shadow, element.shadowRoot);
assert.is(shadow.host, element);
});
test('elements defined with a closed shadow root should expose a null value from the "shadowRoot" property', () => {
class ClosedShadowRoot extends HTMLElement {}
const element = new ClosedShadowRoot();
element.attachShadow({mode: 'closed'});
assert.is(element.shadowRoot, null);
});

test('elements without an attached shadow root should expose a null value from the "shadowRoot" property', () => {
class UnattachedShadowRoot extends HTMLElement {}
const element = new UnattachedShadowRoot();
assert.is(element.shadowRoot, null);
});
test('elements defined with an open shadow root should expose it\'s shadow root from the "shadowRoot" property', () => {
class OpenShadowRoot extends HTMLElement {}
const element = new OpenShadowRoot();
const shadow = element.attachShadow({mode: 'open'});
assert.is(shadow, element.shadowRoot);
assert.is(shadow.host, element);
});
test('elements defined with a closed shadow root should expose a null value from the "shadowRoot" property', () => {
class ClosedShadowRoot extends HTMLElement {}
const element = new ClosedShadowRoot();
element.attachShadow({mode: 'closed'});
assert.is(element.shadowRoot, null);
});
test.run();
}

test.run();
function testWithGlobalShim() {
installWindowOnGlobal({}, false);
const {HTMLElement} = globalThis;
console.log(globalThis);
testWithHtmlElement(HTMLElement);
}
function testWithWindow() {
const window = getWindow({}) as unknown as Window;
const {HTMLElement} = window.window;
window.window;
testWithHtmlElement(HTMLElement);
}

testWithWindow();
testWithGlobalShim();