diff --git a/packages/plugin-ext/src/plugin/preference-registry.spec.ts b/packages/plugin-ext/src/plugin/preference-registry.spec.ts new file mode 100644 index 0000000000000..5dea9fc44ced0 --- /dev/null +++ b/packages/plugin-ext/src/plugin/preference-registry.spec.ts @@ -0,0 +1,83 @@ +/******************************************************************************** + * Copyright (C) 2020 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { PreferenceRegistryExtImpl } from './preference-registry'; +import * as chai from 'chai'; +import { WorkspaceExtImpl } from '../plugin/workspace'; +import { RPCProtocol } from '../common/rpc-protocol'; +import { ProxyIdentifier } from '../common/rpc-protocol'; + +const expect = chai.expect; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +describe('PreferenceRegistryExtImpl:', () => { + + let preferenceRegistryExtImpl: PreferenceRegistryExtImpl; + const getProxy = (proxyId: ProxyIdentifier) => { }; + const set = (identifier: ProxyIdentifier, instance: unknown) => { }; + const dispose = () => { }; + + const mockRPC = { + getProxy, + set, + dispose + + } as RPCProtocol; + const mockWorkspace: WorkspaceExtImpl = {} as WorkspaceExtImpl; + + beforeEach(() => { + preferenceRegistryExtImpl = new PreferenceRegistryExtImpl(mockRPC, mockWorkspace); + }); + + it('Check parseConfigurationData', () => { + const value: { [key: string]: any } = { + 'my.key1.foo': 'value1', + 'my.key1.bar': 'value2', + }; + const result: { [key: string]: any } = (preferenceRegistryExtImpl as any).parseConfigurationData(value); + expect(result.my).to.be.an('object'); + expect(result.my.key1).to.be.an('object'); + + expect(result.my.key1.foo).to.be.an('string'); + expect(result.my.key1.foo).to.equal('value1'); + + expect(result.my.key1.bar).to.be.an('string'); + expect(result.my.key1.bar).to.equal('value2'); + }); + + it('Prototype pollution check', () => { + const value: { [key: string]: any } = { + 'my.key1.foo': 'value1', + '__proto__.injectedParsedPrototype': true, + 'a.__proto__.injectedParsedPrototype': true, + '__proto__': {}, + }; + const result: { [key: string]: any } = (preferenceRegistryExtImpl as any).parseConfigurationData(value); + expect(result.my).to.be.an('object'); + + expect(result.__proto__).to.be.equal(Object.prototype); + + expect(result.my.key1.foo).to.equal('value1'); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const prototypeObject = Object.prototype as any; + expect(prototypeObject.injectedParsedPrototype).to.be.an('undefined'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const rawObject = {} as any; + expect(rawObject.injectedParsedPrototype).to.be.an('undefined'); + }); + +}); diff --git a/packages/plugin-ext/src/plugin/preference-registry.ts b/packages/plugin-ext/src/plugin/preference-registry.ts index 06bbcfa4dfec6..52fd7ddb91adc 100644 --- a/packages/plugin-ext/src/plugin/preference-registry.ts +++ b/packages/plugin-ext/src/plugin/preference-registry.ts @@ -31,6 +31,8 @@ import { Configuration, ConfigurationModel } from './preferences/configuration'; import { WorkspaceExtImpl } from './workspace'; import cloneDeep = require('lodash.clonedeep'); +const prototypePropertyRe = /\b__proto__\b/; + enum ConfigurationTarget { Global = 1, Workspace = 2, @@ -238,7 +240,7 @@ export class PreferenceRegistryExtImpl implements PreferenceRegistryExt { private readonly OVERRIDE_PROPERTY_PATTERN = new RegExp(this.OVERRIDE_PROPERTY); private parseConfigurationData(data: { [key: string]: any }): { [key: string]: any } { - return Object.keys(data).reduce((result: any, key: string) => { + return Object.keys(data).filter(key => !prototypePropertyRe.test(key)).reduce((result: any, key: string) => { const parts = key.split('.'); let branch = result; diff --git a/packages/plugin-ext/src/plugin/preferences/configuration.spec.ts b/packages/plugin-ext/src/plugin/preferences/configuration.spec.ts index b623650ea96d0..4605e30de24e6 100644 --- a/packages/plugin-ext/src/plugin/preferences/configuration.spec.ts +++ b/packages/plugin-ext/src/plugin/preferences/configuration.spec.ts @@ -14,6 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +/* eslint-disable @typescript-eslint/no-explicit-any */ import * as chai from 'chai'; import { Configuration, ConfigurationModel } from './configuration'; import { PreferenceData } from '../../common'; @@ -247,4 +248,35 @@ describe('Configuration:', () => { }); + describe('ConfigurationModel', () => { + + it('check merge', () => { + defaultConfiguration = new ConfigurationModel( + preferences[PreferenceScope.Default], + Object.keys(preferences[PreferenceScope.Default]) + ); + userConfiguration = new ConfigurationModel( + preferences[PreferenceScope.User], + Object.keys(preferences[PreferenceScope.User]) + ); + workspaceConfiguration = new ConfigurationModel( + preferences[PreferenceScope.Workspace], + Object.keys(preferences[PreferenceScope.Workspace]) + ); + const mergedConfiguration = new ConfigurationModel().merge(defaultConfiguration, userConfiguration, workspaceConfiguration); + expect(mergedConfiguration.getValue('tabSize')).to.equal(4); + }); + + it('Prototype pollution check', () => { + const payload = JSON.parse('{"__proto__":{"injectedConfigurationPrototype": true}}'); + const configurationModel = new ConfigurationModel(); + configurationModel.merge(new ConfigurationModel(payload)); + const prototypeObject = Object.prototype as any; + expect(prototypeObject.injectedConfigurationPrototype).to.be.an('undefined'); + const rawObject = {} as any; + expect(rawObject.injectedConfigurationPrototype).to.be.an('undefined'); + }); + + }); + }); diff --git a/packages/plugin-ext/src/plugin/preferences/configuration.ts b/packages/plugin-ext/src/plugin/preferences/configuration.ts index 016ad316e8846..9f5eaae3eefea 100644 --- a/packages/plugin-ext/src/plugin/preferences/configuration.ts +++ b/packages/plugin-ext/src/plugin/preferences/configuration.ts @@ -143,6 +143,9 @@ export class ConfigurationModel { private mergeContents(source: any, target: any): void { for (const key of Object.keys(target)) { + if (key === '__proto__') { + continue; + } if (key in source) { if (isObject(source[key]) && isObject(target[key])) { this.mergeContents(source[key], target[key]);