From 4323d0c7486dff6cb7f7b04dc56b6b64e8da41b5 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Wed, 29 Jul 2026 09:48:01 +0700 Subject: [PATCH] fix(shared): prevent prototype injection in set util --- packages/shared/src/object.test.ts | 31 ++++++++++++++++++++++++++++++ packages/shared/src/object.ts | 22 ++++++++++++++++----- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/packages/shared/src/object.test.ts b/packages/shared/src/object.test.ts index 74f1f4fd9..ab86d36a8 100644 --- a/packages/shared/src/object.test.ts +++ b/packages/shared/src/object.test.ts @@ -144,6 +144,37 @@ describe('set', () => { expect(root.a).toBe(date) expect((root.a as Record).b).toBe('value') }) + + it('does not pollute the prototype via __proto__', () => { + const root: Record = {} + set(root, ['__proto__', 'polluted'], 'yes') + + expect(({} as Record).polluted).toBeUndefined() + expect(Object.getPrototypeOf(root)).toBe(Object.prototype) + expect(Object.hasOwn(root, '__proto__')).toBe(true) + // eslint-disable-next-line no-proto, no-restricted-properties + expect((root as any).__proto__).toEqual({ polluted: 'yes' }) + }) + + it('does not pollute the prototype via constructor.prototype', () => { + const root: Record = {} + set(root, ['constructor', 'prototype', 'polluted'], 'yes') + + expect(({} as Record).polluted).toBeUndefined() + expect((Object.prototype as any).polluted).toBeUndefined() + expect(Object.hasOwn(root, 'constructor')).toBe(true) + expect(root.constructor).toEqual({ prototype: { polluted: 'yes' } }) + }) + + it('sets __proto__ as an own property instead of changing the prototype', () => { + const root: Record = {} + set(root, ['__proto__'], 'value') + + expect(Object.getPrototypeOf(root)).toBe(Object.prototype) + expect(Object.hasOwn(root, '__proto__')).toBe(true) + // eslint-disable-next-line no-proto, no-restricted-properties + expect((root as any).__proto__).toBe('value') + }) }) describe('omit', () => { diff --git a/packages/shared/src/object.ts b/packages/shared/src/object.ts index 63a92ec87..6ff7d5a62 100644 --- a/packages/shared/src/object.ts +++ b/packages/shared/src/object.ts @@ -81,16 +81,28 @@ export function set( for (let i = 0; i < path.length - 1; i++) { const key = path[i]! - const next = (current as Record)[key] + const next = Object.hasOwn(current, key) ? (current as Record)[key] : undefined if (!isTypescriptObject(next)) { - ;(current as Record)[key] = {} + const child = {} + defineOwnProperty(current, key, child) + current = child + } + else { + current = next } - - current = (current as Record)[key]! } - ;(current as Record)[path.at(-1)!] = value + defineOwnProperty(current, path.at(-1)!, value) +} + +function defineOwnProperty(object: object, key: PropertyKey, value: unknown): void { + Object.defineProperty(object, key, { + value, + writable: true, + enumerable: true, + configurable: true, + }) } export function omit(