From 1d7f15ca84b005fedda28a98fddbc3b2d14d60b8 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:32:08 -0700 Subject: [PATCH 1/4] fix: proto could be set during parse --- src/vs/base/common/json.ts | 2 +- src/vs/base/test/common/json.test.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/vs/base/common/json.ts b/src/vs/base/common/json.ts index d7ac069d3d162..1c0b33e958f13 100644 --- a/src/vs/base/common/json.ts +++ b/src/vs/base/common/json.ts @@ -851,7 +851,7 @@ export function parse(text: string, errors: ParseError[] = [], options: ParseOpt function onValue(value: unknown) { if (Array.isArray(currentParent)) { currentParent.push(value); - } else if (currentProperty !== null) { + } else if (currentProperty !== null && currentProperty !== '__proto__') { 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..ce76d734d09ab 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__ property is skipped', () => { + const errors: ParseError[] = []; + const actual = parse('{ "__proto__": { "polluted": true } }', errors); + + assert.deepStrictEqual({ hasOwn: Object.hasOwn(actual, '__proto__'), prototype: Object.getPrototypeOf(actual), errors }, + { hasOwn: false, prototype: Object.prototype, errors: [] }); + }); + test('parse: arrays', () => { assertValidParse('[]', []); assertValidParse('[ [], [ [] ]]', [[], [[]]]); From 3a81c563301942eb5269c9796bbd375c27b7d41f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 23:43:07 +0000 Subject: [PATCH 2/4] fix: preserve __proto__ as own data property in json parse Co-authored-by: rzhao271 <7199958+rzhao271@users.noreply.github.com> --- src/vs/base/common/json.ts | 13 +++++++++++-- src/vs/base/test/common/json.test.ts | 4 ++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/vs/base/common/json.ts b/src/vs/base/common/json.ts index 1c0b33e958f13..589215e4bd29b 100644 --- a/src/vs/base/common/json.ts +++ b/src/vs/base/common/json.ts @@ -851,8 +851,17 @@ export function parse(text: string, errors: ParseError[] = [], options: ParseOpt function onValue(value: unknown) { if (Array.isArray(currentParent)) { currentParent.push(value); - } else if (currentProperty !== null && currentProperty !== '__proto__') { - currentParent[currentProperty] = value; + } else if (currentProperty !== null) { + if (currentProperty === '__proto__') { + Object.defineProperty(currentParent, '__proto__', { + value, + enumerable: true, + writable: true, + configurable: true, + }); + } else { + currentParent[currentProperty] = value; + } } } diff --git a/src/vs/base/test/common/json.test.ts b/src/vs/base/test/common/json.test.ts index ce76d734d09ab..92afa1e16ef5f 100644 --- a/src/vs/base/test/common/json.test.ts +++ b/src/vs/base/test/common/json.test.ts @@ -195,12 +195,12 @@ suite('JSON', () => { assertValidParse('{ "foo": /*hello*/true }', { foo: true }); }); - test('parse: __proto__ property is skipped', () => { + 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: false, prototype: Object.prototype, errors: [] }); + { hasOwn: true, prototype: Object.prototype, errors: [] }); }); test('parse: arrays', () => { From c4891ea870512a42c90e4d52985d64c9f8623453 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Jul 2026 00:45:06 +0000 Subject: [PATCH 3/4] fix: extract setObjectProperty helper and apply to ConfigurationModelParser Co-authored-by: rzhao271 <7199958+rzhao271@users.noreply.github.com> --- src/vs/base/common/json.ts | 29 ++++++++++++------- .../common/configurationModels.ts | 2 +- .../test/common/configurationModels.test.ts | 10 +++++++ 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/vs/base/common/json.ts b/src/vs/base/common/json.ts index 589215e4bd29b..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,16 +870,7 @@ export function parse(text: string, errors: ParseError[] = [], options: ParseOpt if (Array.isArray(currentParent)) { currentParent.push(value); } else if (currentProperty !== null) { - if (currentProperty === '__proto__') { - Object.defineProperty(currentParent, '__proto__', { - value, - enumerable: true, - writable: true, - configurable: true, - }); - } else { - currentParent[currentProperty] = value; - } + setObjectProperty(currentParent, currentProperty, value); } } 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..4a6f74543418d 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(({} as any)['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', () => { From 3e03523f53a8a5c910986d509e5533326930b9fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 18:33:27 +0000 Subject: [PATCH 4/4] Avoid any cast in configuration models prototype pollution test Co-authored-by: rzhao271 <7199958+rzhao271@users.noreply.github.com> --- .../configuration/test/common/configurationModels.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/configuration/test/common/configurationModels.test.ts b/src/vs/platform/configuration/test/common/configurationModels.test.ts index 4a6f74543418d..7a8907090d05a 100644 --- a/src/vs/platform/configuration/test/common/configurationModels.test.ts +++ b/src/vs/platform/configuration/test/common/configurationModels.test.ts @@ -104,7 +104,7 @@ suite('ConfigurationModelParser', () => { testObject.parse(JSON.stringify({ '__proto__': { 'editor.fontSize': 100 } })); - assert.strictEqual(({} as any)['editor.fontSize'], undefined, '__proto__ must not pollute Object.prototype'); + 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 }); });