diff --git a/packages/commons/src/index.ts b/packages/commons/src/index.ts index 9455252384..9d7fecd186 100644 --- a/packages/commons/src/index.ts +++ b/packages/commons/src/index.ts @@ -75,6 +75,19 @@ export const _ = { merge(target: any, source: any) { if (_.isObject(target) && _.isObject(source)) { Object.keys(source).forEach((key) => { + // Skip prototype-chain-mutating keys. `Object.keys` returns + // `__proto__` as an own enumerable when the source object came + // from `JSON.parse('{"__proto__":...}')`; without this filter + // the recursive `_.merge(target[key], source[key])` below + // resolves `target['__proto__']` to `Object.prototype` and + // writes attacker-controlled keys onto it. + if ( + key === '__proto__' || + key === 'constructor' || + key === 'prototype' + ) { + return + } if (_.isObject(source[key])) { if (!target[key]) { Object.assign(target, { [key]: {} }) diff --git a/packages/commons/test/utils.test.ts b/packages/commons/test/utils.test.ts index 53d51978ab..b7724551d0 100644 --- a/packages/commons/test/utils.test.ts +++ b/packages/commons/test/utils.test.ts @@ -212,5 +212,25 @@ describe('@feathersjs/commons utils', () => { assert.equal(_.merge('hello', {}), 'hello') }) + + it('merge does not mutate Object.prototype via __proto__ keys', () => { + // `Object.keys` returns `__proto__` as an own enumerable key when the + // source object came from `JSON.parse('{"__proto__":...}')`. Without + // the filter in the merge implementation, the recursive call resolves + // `target['__proto__']` to `Object.prototype` and writes attacker- + // controlled keys onto it (affecting every plain object in the + // process). + const target = {} as any + _.merge(target, JSON.parse('{"__proto__":{"polluted":"X"}}')) + assert.strictEqual(({} as any).polluted, undefined) + assert.strictEqual((Object.prototype as any).polluted, undefined) + assert.strictEqual(target.polluted, undefined) + + // Same protection for `constructor` and `prototype` keys (the rest + // of the standard prototype-mutating triad). + _.merge(target, JSON.parse('{"constructor":{"prototype":{"polluted2":"Y"}}}')) + assert.strictEqual(({} as any).polluted2, undefined) + assert.strictEqual((Object.prototype as any).polluted2, undefined) + }) }) })