From 26078db6e92cdc79bb6b847e681798ae3a188752 Mon Sep 17 00:00:00 2001 From: Cris Staicu Date: Tue, 17 Mar 2026 09:30:18 +0200 Subject: [PATCH] Prevent writes to forbidden properties --- lib/nconf/stores/memory.js | 17 +++++++++++ test/stores/memory-store.test.js | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/lib/nconf/stores/memory.js b/lib/nconf/stores/memory.js index 34bab8a5..c575441b 100644 --- a/lib/nconf/stores/memory.js +++ b/lib/nconf/stores/memory.js @@ -15,6 +15,11 @@ function escapeRegExp(string) { return typeof string === 'string' && string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string } +// Guard against prototype pollution via dangerous key segments +function isSafeKey(key) { + return key !== '__proto__' && key !== 'constructor' && key !== 'prototype'; +} + // // ### function Memory (options) // #### @options {Object} Options for this instance @@ -123,6 +128,9 @@ Memory.prototype.set = function (key, value) { // while (path.length > 1) { key = path.shift(); + if (!isSafeKey(key)) { + return false; + } if (!target[key] || typeof target[key] !== 'object') { target[key] = {}; } @@ -132,6 +140,9 @@ Memory.prototype.set = function (key, value) { // Set the specified value in the nested JSON structure key = path.shift(); + if (!isSafeKey(key)) { + return false; + } if (this.parseValues) { value = common.parseValues.call(common, value); } @@ -212,6 +223,9 @@ Memory.prototype.merge = function (key, value) { // while (path.length > 1) { key = path.shift(); + if (!isSafeKey(key)) { + return false; + } if (!target[key]) { target[key] = {}; } @@ -221,6 +235,9 @@ Memory.prototype.merge = function (key, value) { // Set the specified value in the nested JSON structure key = path.shift(); + if (!isSafeKey(key)) { + return false; + } // // If the current value at the key target is not an `Object`, diff --git a/test/stores/memory-store.test.js b/test/stores/memory-store.test.js index 7262035d..d122b738 100644 --- a/test/stores/memory-store.test.js +++ b/test/stores/memory-store.test.js @@ -96,6 +96,54 @@ describe('nconf/stores/memory', () => { }); }); }); + describe("prototype pollution prevention", () => { + // merge() uses common.path() directly (no _normalizeKey), so __proto__ reaches + // the traversal loop as-is and must be blocked by isSafeKey(). + const dangerousKeys = [ + '__proto__:polluted', + 'constructor:polluted', + 'prototype:polluted', + 'a:__proto__:polluted', + 'a:constructor:polluted', + 'a:prototype:polluted', + ]; + + it("merge() should return false and not pollute Object.prototype", () => { + const store = new nconf.Memory(); + for (const key of dangerousKeys) { + expect(store.merge(key, { value: 'injected' })).toBe(false); + } + expect(({}).polluted).toBeUndefined(); + }); + + it("nconf.merge() with __proto__ key should not pollute Object.prototype", () => { + nconf.merge('__proto__:polluted', { value: 'yes' }); + expect(({}).polluted).toBeUndefined(); + }); + + // set() runs _normalizeKey() first (which replaces '__' input separator), + // so __proto__ is transformed before traversal. Test that no pollution occurs + // regardless of normalisation behaviour. + it("set() should not pollute Object.prototype", () => { + const store = new nconf.Memory(); + for (const key of dangerousKeys) { + store.set(key, { value: 'injected' }); + } + expect(({}).polluted).toBeUndefined(); + }); + + // With a custom separator config where '__' is NOT the input separator, + // '__proto__' passes through _normalizeKey unchanged and isSafeKey() is + // the only guard — it must return false. + it("set() should return false for dangerous keys when '__' is not the input separator", () => { + const store = new nconf.Memory({ inputSeparator: '-', disableDefaultAccessSeparator: true }); + expect(store.set('__proto__:polluted', { value: 'injected' })).toBe(false); + expect(store.set('constructor:polluted', { value: 'injected' })).toBe(false); + expect(store.set('prototype:polluted', { value: 'injected' })).toBe(false); + expect(({}).polluted).toBeUndefined(); + }); + }); + describe("When using the nconf memory store with different logical separator", () => { var store = new nconf.Memory({ accessSeparator: '||', disableDefaultAccessSeparator: true });