From f9b774a063e8fe5cf9006b9d4f9483d0ab230b21 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sat, 9 May 2026 02:57:30 +0000 Subject: [PATCH] fix(nested-obj): guard against prototype pollution in path traversal Reject unsafe path segments (__proto__, constructor, prototype) in get, set, and has methods to prevent prototype pollution attacks. Extracts path parsing into a shared parsePath helper that validates all segments before traversal. CWE-1321 / CVSS 3.1 8.2 High --- packages/nested-obj/__tests__/options.test.ts | 28 +++++++++++++++++++ packages/nested-obj/src/index.ts | 18 ++++++++++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/nested-obj/__tests__/options.test.ts b/packages/nested-obj/__tests__/options.test.ts index 81bb50a5..bee6c6dc 100644 --- a/packages/nested-obj/__tests__/options.test.ts +++ b/packages/nested-obj/__tests__/options.test.ts @@ -57,6 +57,34 @@ describe('Object Path Operations', () => { }); }); + describe('prototype pollution protection', () => { + it('should throw on __proto__ path segment in set', () => { + expect(() => objectPath.set(obj, '__proto__.polluted', 'yes')).toThrow('Unsafe path segment: __proto__'); + expect(({} as any).polluted).toBeUndefined(); + }); + + it('should throw on constructor path segment in set', () => { + expect(() => objectPath.set(obj, 'constructor.polluted', 'yes')).toThrow('Unsafe path segment: constructor'); + }); + + it('should throw on prototype path segment in set', () => { + expect(() => objectPath.set(obj, 'prototype.polluted', 'yes')).toThrow('Unsafe path segment: prototype'); + }); + + it('should throw on __proto__ path segment in get', () => { + expect(() => objectPath.get(obj, '__proto__.polluted')).toThrow('Unsafe path segment: __proto__'); + }); + + it('should throw on __proto__ path segment in has', () => { + expect(() => objectPath.has(obj, '__proto__.polluted')).toThrow('Unsafe path segment: __proto__'); + }); + + it('should throw on nested unsafe path segments', () => { + expect(() => objectPath.set(obj, 'user.__proto__.polluted', 'yes')).toThrow('Unsafe path segment: __proto__'); + expect(() => objectPath.set(obj, 'user.constructor.polluted', 'yes')).toThrow('Unsafe path segment: constructor'); + }); + }); + describe('has', () => { it('should return true if a path exists', () => { expect(objectPath.has(obj, 'user.name')).toBe(true); diff --git a/packages/nested-obj/src/index.ts b/packages/nested-obj/src/index.ts index 12888fc1..e3522505 100644 --- a/packages/nested-obj/src/index.ts +++ b/packages/nested-obj/src/index.ts @@ -1,6 +1,18 @@ +const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']); + +function parsePath(path: string): string[] { + const keys = path.replace(/\[(\w+)\]/g, '.$1').split('.'); + for (const key of keys) { + if (UNSAFE_KEYS.has(key)) { + throw new Error('Unsafe path segment: ' + key); + } + } + return keys; +} + export default { get(obj: Record, path: string): T | undefined { - const keys = path.replace(/\[(\w+)\]/g, '.$1').split('.'); + const keys = parsePath(path); let result: any = obj; for (const key of keys) { if (result == null) { @@ -16,7 +28,7 @@ export default { return; } - const keys = path.replace(/\[(\w+)\]/g, '.$1').split('.'); + const keys = parsePath(path); let current = obj; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; @@ -29,7 +41,7 @@ export default { }, has(obj: Record, path: string): boolean { - const keys = path.replace(/\[(\w+)\]/g, '.$1').split('.'); + const keys = parsePath(path); let current = obj; for (const key of keys) { if (current == null || !(key in current)) {