Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,13 @@ export const LightningElement: LightningElementConstructor = function (
);
}

// Prevent constructor from being invoked as a function (e.g. LightningElement.call(iframe)),
// which would allow overwriting vm.component and bypass LWC sandbox. Allow a single function-style
// call for Locker/mirror integrations (SecureBase that does LightningElement.prototype.constructor.call(this)).
if (!lwcRuntimeFlags.DISABLE_CONSTRUCTOR_INVOCATION_VALIDATION && !new.target) {
throw new TypeError('Cannot call LightningElement constructor.');
}

setPrototypeOf(elm, bridge.prototype);

vm.component = this;
Expand Down
18 changes: 2 additions & 16 deletions packages/@lwc/engine-core/src/framework/invoker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { addErrorComponentStack } from '../shared/error';
import { evaluateTemplate, setVMBeingRendered, getVMBeingRendered } from './template';
import { runWithBoundaryProtection } from './vm';
import { logOperationStart, logOperationEnd, OperationId } from './profiler';
import { LightningElement } from './base-lightning-element';
import type { LightningElement } from './base-lightning-element';
import type { Template } from './template';
import type { VM } from './vm';
import type { LightningElementConstructor } from './base-lightning-element';
Expand Down Expand Up @@ -51,22 +51,8 @@ export function invokeComponentConstructor(vm: VM, Ctor: LightningElementConstru
* associated to the diffing algo.
*/
try {
// job
const result = new Ctor();

// Check indirectly if the constructor result is an instance of LightningElement.
// When Locker is enabled, the "instanceof" operator would not work since Locker Service
// provides its own implementation of LightningElement, so we indirectly check
// if the base constructor is invoked by accessing the component on the vm.
// When the DISABLE_LOCKER_VALIDATION gate is false or LEGACY_LOCKER_ENABLED is false,
// then the instanceof LightningElement can be used.
const useLegacyConstructorCheck =
!lwcRuntimeFlags.DISABLE_LEGACY_VALIDATION || lwcRuntimeFlags.LEGACY_LOCKER_ENABLED;

const isInvalidConstructor = useLegacyConstructorCheck
? vmBeingConstructed.component !== result
: !(result instanceof LightningElement);

const isInvalidConstructor = vmBeingConstructed.component !== result;
if (isInvalidConstructor) {
throw new TypeError(
'Invalid component constructor, the class should extend LightningElement.'
Expand Down
3 changes: 1 addition & 2 deletions packages/@lwc/features/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ const features: FeatureFlagMap = {
ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION: null,
DISABLE_SYNTHETIC_SHADOW: null,
DISABLE_SCOPE_TOKEN_VALIDATION: null,
LEGACY_LOCKER_ENABLED: null,
DISABLE_LEGACY_VALIDATION: null,
DISABLE_CONSTRUCTOR_INVOCATION_VALIDATION: null,
DISABLE_DETACHED_REHYDRATION: null,
ENABLE_LEGACY_CONTEXT_CONNECTION: null,
};
Expand Down
14 changes: 4 additions & 10 deletions packages/@lwc/features/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,11 @@ export interface FeatureFlagMap {
DISABLE_SCOPE_TOKEN_VALIDATION: FeatureFlagValue;

/**
* If true, then lightning legacy locker is supported, otherwise lightning legacy locker will not function
* properly.
* If true, disables the LightningElement constructor invocation validation (the new.target check
* that prevents calling the constructor as a function, e.g. LightningElement.call(iframe)).
* Default is false: validation is enabled and constructor must be invoked with `new`.
*/
LEGACY_LOCKER_ENABLED: FeatureFlagValue;

/**
* A manual override for `LEGACY_LOCKER_ENABLED`; should not be used if that flag is correctly set.
* If true, behave as if legacy Locker is enabled.
* If false or unset, then the value of the `LEGACY_LOCKER_ENABLED` flag is used.
*/
DISABLE_LEGACY_VALIDATION: FeatureFlagValue;
DISABLE_CONSTRUCTOR_INVOCATION_VALIDATION: FeatureFlagValue;

/**
* If true, skips rehydration of DOM elements that are not connected.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createElement, LightningElement, setFeatureFlagForTest } from 'lwc';
import { createElement, LightningElement } from 'lwc';

import Test from 'x/test';
import ShadowRootGetter from 'x/shadowRootGetter';
Expand Down Expand Up @@ -100,12 +100,6 @@ describe.runIf(process.env.NATIVE_SHADOW)('native shadow', () => {
});

describe('locker integration', () => {
beforeEach(() => {
setFeatureFlagForTest('LEGACY_LOCKER_ENABLED', true);
});
afterEach(() => {
setFeatureFlagForTest('LEGACY_LOCKER_ENABLED', false);
});
it('should support component class that extend a mirror of the LightningElement', () => {
function SecureBaseClass() {
if (this instanceof SecureBaseClass) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,37 +81,40 @@ it("[W-6981076] shouldn't throw when a component with an invalid child in unmoun
expect(() => document.body.removeChild(elm)).not.toThrow();
});

it('should fail when the constructor returns something other than LightningElement when DISABLE_LEGACY_VALIDATION is true and LEGACY_LOCKER_ENABLED is falsy', () => {
setFeatureFlagForTest('DISABLE_LEGACY_VALIDATION', true);
it('should throw when the constructor returns something other than LightningElement', () => {
expect(() => {
createElement('x-returning-bad', { is: ReturningBad });
}).toThrowError(
TypeError,
'Invalid component constructor, the class should extend LightningElement.'
);
setFeatureFlagForTest('DISABLE_LEGACY_VALIDATION', false);
});

it('should succeed when the constructor returns something other than LightningElement when DISABLE_LEGACY_VALIDATION is falsy and LEGACY_LOCKER_ENABLED is falsy', () => {
it('should succeed when the constructor returns something other than LightningElement when DISABLE_CONSTRUCTOR_INVOCATION_VALIDATION is true', () => {
setFeatureFlagForTest('DISABLE_CONSTRUCTOR_INVOCATION_VALIDATION', true);
expect(() => {
createElement('x-returning-bad', { is: ReturningBad });
}).not.toThrow();
setFeatureFlagForTest('DISABLE_CONSTRUCTOR_INVOCATION_VALIDATION', false);
});

it('should succeed when the constructor returns something other than LightningElement when DISABLE_LEGACY_VALIDATION is falsy and LEGACY_LOCKER_ENABLED is true', () => {
setFeatureFlagForTest('LEGACY_LOCKER_ENABLED', true);
expect(() => {
createElement('x-returning-bad', { is: ReturningBad });
}).not.toThrow();
setFeatureFlagForTest('LEGACY_LOCKER_ENABLED', false);
it('should throw when calling LightningElement constructor as a function (e.g. .call/.apply)', () => {
const func = () => {
const fakeThis = {};
LightningElement.call(fakeThis);
};
expect(func).toThrowError(TypeError);
expect(func).toThrowError(/Cannot call LightningElement constructor/);
});

it('should succeed when the constructor returns something other than LightningElement when DISABLE_LEGACY_VALIDATION is true and LEGACY_LOCKER_ENABLED is true', () => {
setFeatureFlagForTest('DISABLE_LEGACY_VALIDATION', true);
setFeatureFlagForTest('LEGACY_LOCKER_ENABLED', true);
expect(() => {
createElement('x-returning-bad', { is: ReturningBad });
}).not.toThrow();
setFeatureFlagForTest('DISABLE_LEGACY_VALIDATION', false);
setFeatureFlagForTest('LEGACY_LOCKER_ENABLED', false);
it('should throw Illegal constructor (not Cannot call) when DISABLE_CONSTRUCTOR_INVOCATION_VALIDATION is true', () => {
setFeatureFlagForTest('DISABLE_CONSTRUCTOR_INVOCATION_VALIDATION', true);
const func = () => {
const fakeThis = {};
LightningElement.call(fakeThis);
};
expect(func).toThrowError(TypeError);
expect(func).toThrowError(/Illegal constructor/);
expect(func).not.toThrowError(/Cannot call LightningElement constructor/);
setFeatureFlagForTest('DISABLE_CONSTRUCTOR_INVOCATION_VALIDATION', false);
});
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { createElement, setFeatureFlagForTest } from 'lwc';
import { createElement } from 'lwc';

import LockerIntegration from 'x/lockerIntegration';
import LockerLiveComponent from 'x/lockerLiveComponent';
import LockerHooks, { hooks } from 'x/lockerHooks';
import { spyOn } from '@vitest/spy';
beforeEach(() => {
setFeatureFlagForTest('LEGACY_LOCKER_ENABLED', true);
});
afterEach(() => {
setFeatureFlagForTest('LEGACY_LOCKER_ENABLED', false);
});
it('should support Locker integration which uses a wrapped LightningElement base class', () => {
const elm = createElement('x-secure-parent', { is: LockerIntegration });
document.body.appendChild(elm);
Expand Down
Loading