diff --git a/packages/@lwc/engine-core/src/framework/base-lightning-element.ts b/packages/@lwc/engine-core/src/framework/base-lightning-element.ts index 6e46a76c79..8c3b310ee2 100644 --- a/packages/@lwc/engine-core/src/framework/base-lightning-element.ts +++ b/packages/@lwc/engine-core/src/framework/base-lightning-element.ts @@ -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; diff --git a/packages/@lwc/engine-core/src/framework/invoker.ts b/packages/@lwc/engine-core/src/framework/invoker.ts index 822ed36966..a4f8971025 100644 --- a/packages/@lwc/engine-core/src/framework/invoker.ts +++ b/packages/@lwc/engine-core/src/framework/invoker.ts @@ -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'; @@ -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.' diff --git a/packages/@lwc/features/src/index.ts b/packages/@lwc/features/src/index.ts index 0bbf1cbe3b..9a562af92f 100644 --- a/packages/@lwc/features/src/index.ts +++ b/packages/@lwc/features/src/index.ts @@ -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, }; diff --git a/packages/@lwc/features/src/types.ts b/packages/@lwc/features/src/types.ts index 56813af5b9..6158554a7d 100644 --- a/packages/@lwc/features/src/types.ts +++ b/packages/@lwc/features/src/types.ts @@ -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. diff --git a/packages/@lwc/integration-wtr/test/api/createElement/index.spec.js b/packages/@lwc/integration-wtr/test/api/createElement/index.spec.js index bf4c3485d8..715a385bbe 100644 --- a/packages/@lwc/integration-wtr/test/api/createElement/index.spec.js +++ b/packages/@lwc/integration-wtr/test/api/createElement/index.spec.js @@ -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'; @@ -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) { diff --git a/packages/@lwc/integration-wtr/test/component/LightningElement/index.spec.js b/packages/@lwc/integration-wtr/test/component/LightningElement/index.spec.js index 2fe385dc68..10bc8c5c54 100644 --- a/packages/@lwc/integration-wtr/test/component/LightningElement/index.spec.js +++ b/packages/@lwc/integration-wtr/test/component/LightningElement/index.spec.js @@ -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); }); diff --git a/packages/@lwc/integration-wtr/test/integrations/locker/index.spec.js b/packages/@lwc/integration-wtr/test/integrations/locker/index.spec.js index c022a78663..70c21c27b4 100644 --- a/packages/@lwc/integration-wtr/test/integrations/locker/index.spec.js +++ b/packages/@lwc/integration-wtr/test/integrations/locker/index.spec.js @@ -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);