Skip to content

Commit 5c91bbc

Browse files
fix: constructor security issue
1 parent 931322f commit 5c91bbc

File tree

7 files changed

+37
-60
lines changed

7 files changed

+37
-60
lines changed

packages/@lwc/engine-core/src/framework/base-lightning-element.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,13 @@ export const LightningElement: LightningElementConstructor = function (
251251
);
252252
}
253253

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

256263
vm.component = this;

packages/@lwc/engine-core/src/framework/invoker.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { addErrorComponentStack } from '../shared/error';
1111
import { evaluateTemplate, setVMBeingRendered, getVMBeingRendered } from './template';
1212
import { runWithBoundaryProtection } from './vm';
1313
import { logOperationStart, logOperationEnd, OperationId } from './profiler';
14-
import { LightningElement } from './base-lightning-element';
14+
import type { LightningElement } from './base-lightning-element';
1515
import type { Template } from './template';
1616
import type { VM } from './vm';
1717
import type { LightningElementConstructor } from './base-lightning-element';
@@ -51,22 +51,8 @@ export function invokeComponentConstructor(vm: VM, Ctor: LightningElementConstru
5151
* associated to the diffing algo.
5252
*/
5353
try {
54-
// job
5554
const result = new Ctor();
56-
57-
// Check indirectly if the constructor result is an instance of LightningElement.
58-
// When Locker is enabled, the "instanceof" operator would not work since Locker Service
59-
// provides its own implementation of LightningElement, so we indirectly check
60-
// if the base constructor is invoked by accessing the component on the vm.
61-
// When the DISABLE_LOCKER_VALIDATION gate is false or LEGACY_LOCKER_ENABLED is false,
62-
// then the instanceof LightningElement can be used.
63-
const useLegacyConstructorCheck =
64-
!lwcRuntimeFlags.DISABLE_LEGACY_VALIDATION || lwcRuntimeFlags.LEGACY_LOCKER_ENABLED;
65-
66-
const isInvalidConstructor = useLegacyConstructorCheck
67-
? vmBeingConstructed.component !== result
68-
: !(result instanceof LightningElement);
69-
55+
const isInvalidConstructor = vmBeingConstructed.component !== result;
7056
if (isInvalidConstructor) {
7157
throw new TypeError(
7258
'Invalid component constructor, the class should extend LightningElement.'

packages/@lwc/features/src/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ const features: FeatureFlagMap = {
2222
ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION: null,
2323
DISABLE_SYNTHETIC_SHADOW: null,
2424
DISABLE_SCOPE_TOKEN_VALIDATION: null,
25-
LEGACY_LOCKER_ENABLED: null,
26-
DISABLE_LEGACY_VALIDATION: null,
25+
DISABLE_CONSTRUCTOR_INVOCATION_VALIDATION: null,
2726
DISABLE_DETACHED_REHYDRATION: null,
2827
ENABLE_LEGACY_CONTEXT_CONNECTION: null,
2928
};

packages/@lwc/features/src/types.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -89,17 +89,11 @@ export interface FeatureFlagMap {
8989
DISABLE_SCOPE_TOKEN_VALIDATION: FeatureFlagValue;
9090

9191
/**
92-
* If true, then lightning legacy locker is supported, otherwise lightning legacy locker will not function
93-
* properly.
92+
* If true, disables the LightningElement constructor invocation validation (the new.target check
93+
* that prevents calling the constructor as a function, e.g. LightningElement.call(iframe)).
94+
* Default is false: validation is enabled and constructor must be invoked with `new`.
9495
*/
95-
LEGACY_LOCKER_ENABLED: FeatureFlagValue;
96-
97-
/**
98-
* A manual override for `LEGACY_LOCKER_ENABLED`; should not be used if that flag is correctly set.
99-
* If true, behave as if legacy Locker is enabled.
100-
* If false or unset, then the value of the `LEGACY_LOCKER_ENABLED` flag is used.
101-
*/
102-
DISABLE_LEGACY_VALIDATION: FeatureFlagValue;
96+
DISABLE_CONSTRUCTOR_INVOCATION_VALIDATION: FeatureFlagValue;
10397

10498
/**
10599
* If true, skips rehydration of DOM elements that are not connected.

packages/@lwc/integration-wtr/test/api/createElement/index.spec.js

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createElement, LightningElement, setFeatureFlagForTest } from 'lwc';
1+
import { createElement, LightningElement } from 'lwc';
22

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

102102
describe('locker integration', () => {
103-
beforeEach(() => {
104-
setFeatureFlagForTest('LEGACY_LOCKER_ENABLED', true);
105-
});
106-
afterEach(() => {
107-
setFeatureFlagForTest('LEGACY_LOCKER_ENABLED', false);
108-
});
109103
it('should support component class that extend a mirror of the LightningElement', () => {
110104
function SecureBaseClass() {
111105
if (this instanceof SecureBaseClass) {

packages/@lwc/integration-wtr/test/component/LightningElement/index.spec.js

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -81,37 +81,40 @@ it("[W-6981076] shouldn't throw when a component with an invalid child in unmoun
8181
expect(() => document.body.removeChild(elm)).not.toThrow();
8282
});
8383

84-
it('should fail when the constructor returns something other than LightningElement when DISABLE_LEGACY_VALIDATION is true and LEGACY_LOCKER_ENABLED is falsy', () => {
85-
setFeatureFlagForTest('DISABLE_LEGACY_VALIDATION', true);
84+
it('should throw when the constructor returns something other than LightningElement', () => {
8685
expect(() => {
8786
createElement('x-returning-bad', { is: ReturningBad });
8887
}).toThrowError(
8988
TypeError,
9089
'Invalid component constructor, the class should extend LightningElement.'
9190
);
92-
setFeatureFlagForTest('DISABLE_LEGACY_VALIDATION', false);
9391
});
9492

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

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

109-
it('should succeed when the constructor returns something other than LightningElement when DISABLE_LEGACY_VALIDATION is true and LEGACY_LOCKER_ENABLED is true', () => {
110-
setFeatureFlagForTest('DISABLE_LEGACY_VALIDATION', true);
111-
setFeatureFlagForTest('LEGACY_LOCKER_ENABLED', true);
112-
expect(() => {
113-
createElement('x-returning-bad', { is: ReturningBad });
114-
}).not.toThrow();
115-
setFeatureFlagForTest('DISABLE_LEGACY_VALIDATION', false);
116-
setFeatureFlagForTest('LEGACY_LOCKER_ENABLED', false);
110+
it('should throw Illegal constructor (not Cannot call) when DISABLE_CONSTRUCTOR_INVOCATION_VALIDATION is true', () => {
111+
setFeatureFlagForTest('DISABLE_CONSTRUCTOR_INVOCATION_VALIDATION', true);
112+
const func = () => {
113+
const fakeThis = {};
114+
LightningElement.call(fakeThis);
115+
};
116+
expect(func).toThrowError(TypeError);
117+
expect(func).toThrowError(/Illegal constructor/);
118+
expect(func).not.toThrowError(/Cannot call LightningElement constructor/);
119+
setFeatureFlagForTest('DISABLE_CONSTRUCTOR_INVOCATION_VALIDATION', false);
117120
});

packages/@lwc/integration-wtr/test/integrations/locker/index.spec.js

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
1-
import { createElement, setFeatureFlagForTest } from 'lwc';
1+
import { createElement } from 'lwc';
22

33
import LockerIntegration from 'x/lockerIntegration';
44
import LockerLiveComponent from 'x/lockerLiveComponent';
55
import LockerHooks, { hooks } from 'x/lockerHooks';
66
import { spyOn } from '@vitest/spy';
7-
beforeEach(() => {
8-
setFeatureFlagForTest('LEGACY_LOCKER_ENABLED', true);
9-
});
10-
afterEach(() => {
11-
setFeatureFlagForTest('LEGACY_LOCKER_ENABLED', false);
12-
});
137
it('should support Locker integration which uses a wrapped LightningElement base class', () => {
148
const elm = createElement('x-secure-parent', { is: LockerIntegration });
159
document.body.appendChild(elm);

0 commit comments

Comments
 (0)