diff --git a/src/vs/base/common/json.ts b/src/vs/base/common/json.ts index d7ac069d3d162..11743e4bcbdb5 100644 --- a/src/vs/base/common/json.ts +++ b/src/vs/base/common/json.ts @@ -839,6 +839,24 @@ export function getLocation(text: string, position: number): Location { } +/** + * Safely assigns `value` to `obj[key]`, using `Object.defineProperty` for the + * `__proto__` key so that the own property is preserved without triggering the + * prototype setter (prototype pollution). + */ +export function setObjectProperty(obj: Record, key: string, value: unknown): void { + if (key === '__proto__') { + Object.defineProperty(obj, '__proto__', { + value, + enumerable: true, + writable: true, + configurable: true, + }); + } else { + obj[key] = value; + } +} + /** * Parses the given text and returns the object the JSON content represents. On invalid input, the parser tries to be as fault tolerant as possible, but still return a result. * Therefore always check the errors list to find out if the input was valid. @@ -852,7 +870,7 @@ export function parse(text: string, errors: ParseError[] = [], options: ParseOpt if (Array.isArray(currentParent)) { currentParent.push(value); } else if (currentProperty !== null) { - currentParent[currentProperty] = value; + setObjectProperty(currentParent, currentProperty, value); } } diff --git a/src/vs/base/test/common/json.test.ts b/src/vs/base/test/common/json.test.ts index ff75a79a73edd..92afa1e16ef5f 100644 --- a/src/vs/base/test/common/json.test.ts +++ b/src/vs/base/test/common/json.test.ts @@ -195,6 +195,14 @@ suite('JSON', () => { assertValidParse('{ "foo": /*hello*/true }', { foo: true }); }); + test('parse: __proto__ own property is preserved without polluting prototype', () => { + const errors: ParseError[] = []; + const actual = parse('{ "__proto__": { "polluted": true } }', errors); + + assert.deepStrictEqual({ hasOwn: Object.hasOwn(actual, '__proto__'), prototype: Object.getPrototypeOf(actual), errors }, + { hasOwn: true, prototype: Object.prototype, errors: [] }); + }); + test('parse: arrays', () => { assertValidParse('[]', []); assertValidParse('[ [], [ [] ]]', [[], [[]]]); diff --git a/src/vs/platform/configuration/common/configurationModels.ts b/src/vs/platform/configuration/common/configurationModels.ts index b5cf8e4a264db..be34cd2ebb0e3 100644 --- a/src/vs/platform/configuration/common/configurationModels.ts +++ b/src/vs/platform/configuration/common/configurationModels.ts @@ -369,7 +369,7 @@ export class ConfigurationModelParser { if (Array.isArray(currentParent)) { currentParent.push(value); } else if (currentProperty !== null) { - currentParent[currentProperty] = value; + json.setObjectProperty(currentParent as Record, currentProperty, value); } } diff --git a/src/vs/platform/configuration/test/common/configurationModels.test.ts b/src/vs/platform/configuration/test/common/configurationModels.test.ts index f3538bb0ac0d3..7a8907090d05a 100644 --- a/src/vs/platform/configuration/test/common/configurationModels.test.ts +++ b/src/vs/platform/configuration/test/common/configurationModels.test.ts @@ -99,6 +99,16 @@ suite('ConfigurationModelParser', () => { assert.strictEqual(testObject.configurationModel.getValue('a.b.c'), undefined); }); + test('parse configuration model with __proto__ key does not cause prototype pollution', () => { + const testObject = new ConfigurationModelParser('', new NullLogService()); + + testObject.parse(JSON.stringify({ '__proto__': { 'editor.fontSize': 100 } })); + + assert.strictEqual((Object.prototype as Record)['editor.fontSize'], undefined, '__proto__ must not pollute Object.prototype'); + const raw = testObject.configurationModel.getValue('__proto__') as Record; + assert.deepStrictEqual(raw, { 'editor.fontSize': 100 }); + }); + }); suite('ConfigurationModelParser - Excluded Properties', () => {