Skip to content

Commit 9cf727f

Browse files
authored
Prevent __proto__ pollution in util.deepExtend (#4001)
* do not extend __proto__ * replace asserts with expects * Create fuzzy-impalas-brake.md * address comment
1 parent ab0f643 commit 9cf727f

File tree

3 files changed

+53
-33
lines changed

3 files changed

+53
-33
lines changed

.changeset/fuzzy-impalas-brake.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@firebase/util": patch
3+
---
4+
5+
Do not merge `__proto__` in `deepExtend` to prevent `__proto__` pollution.

packages/util/src/deepCopy.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export function deepCopy<T>(value: T): T {
3333
*
3434
* Note that the target can be a function, in which case the properties in
3535
* the source Object are copied onto it as static properties of the Function.
36+
*
37+
* Note: we don't merge __proto__ to prevent prototype pollution
3638
*/
3739
export function deepExtend(target: unknown, source: unknown): unknown {
3840
if (!(source instanceof Object)) {
@@ -62,14 +64,19 @@ export function deepExtend(target: unknown, source: unknown): unknown {
6264
}
6365

6466
for (const prop in source) {
65-
if (!source.hasOwnProperty(prop)) {
67+
// use isValidKey to guard against prototype pollution. See https://snyk.io/vuln/SNYK-JS-LODASH-450202
68+
if (!source.hasOwnProperty(prop) || !isValidKey(prop)) {
6669
continue;
6770
}
68-
(target as { [key: string]: unknown })[prop] = deepExtend(
69-
(target as { [key: string]: unknown })[prop],
70-
(source as { [key: string]: unknown })[prop]
71+
(target as Record<string, unknown>)[prop] = deepExtend(
72+
(target as Record<string, unknown>)[prop],
73+
(source as Record<string, unknown>)[prop]
7174
);
7275
}
7376

7477
return target;
7578
}
79+
80+
function isValidKey(key: string): boolean {
81+
return key !== '__proto__';
82+
}

packages/util/test/deepCopy.test.ts

+37-29
Original file line numberDiff line numberDiff line change
@@ -14,78 +14,77 @@
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17-
import { assert } from 'chai';
17+
import { expect } from 'chai';
1818
import { deepCopy, deepExtend } from '../src/deepCopy';
1919

2020
describe('deepCopy()', () => {
2121
it('Scalars', () => {
22-
assert.strictEqual(deepCopy(true), true);
23-
assert.strictEqual(deepCopy(123), 123);
24-
assert.strictEqual(deepCopy('abc'), 'abc');
22+
expect(deepCopy(true)).to.equal(true);
23+
expect(deepCopy(123)).to.equal(123);
24+
expect(deepCopy('abc')).to.equal('abc');
2525
});
2626

2727
it('Date', () => {
2828
const d = new Date();
29-
assert.deepEqual(deepCopy(d), d);
29+
expect(deepCopy(d)).to.deep.equal(d);
3030
});
3131

3232
it('Object', () => {
33-
assert.deepEqual(deepCopy({}), {});
34-
assert.deepEqual(deepCopy({ a: 123 }), { a: 123 });
35-
assert.deepEqual(deepCopy({ a: { b: 123 } }), { a: { b: 123 } });
33+
expect(deepCopy({})).to.deep.equal({});
34+
expect(deepCopy({ a: 123 })).to.deep.equal({ a: 123 });
35+
expect(deepCopy({ a: { b: 123 } })).to.deep.equal({ a: { b: 123 } });
3636
});
3737

3838
it('Array', () => {
39-
assert.deepEqual(deepCopy([]), []);
40-
assert.deepEqual(deepCopy([123, 456]), [123, 456]);
41-
assert.deepEqual(deepCopy([123, [456]]), [123, [456]]);
39+
expect(deepCopy([])).to.deep.equal([]);
40+
expect(deepCopy([123, 456])).to.deep.equal([123, 456]);
41+
expect(deepCopy([123, [456]])).to.deep.equal([123, [456]]);
4242
});
4343
});
4444

4545
describe('deepExtend', () => {
4646
it('Scalars', () => {
47-
assert.strictEqual(deepExtend(1, true), true);
48-
assert.strictEqual(deepExtend(undefined, 123), 123);
49-
assert.strictEqual(deepExtend('was', 'abc'), 'abc');
47+
expect(deepExtend(1, true)).to.equal(true);
48+
expect(deepExtend(undefined, 123)).to.equal(123);
49+
expect(deepExtend('was', 'abc')).to.equal('abc');
5050
});
5151

5252
it('Date', () => {
5353
const d = new Date();
54-
assert.deepEqual(deepExtend(new Date(), d), d);
54+
expect(deepExtend(new Date(), d)).to.deep.equal(d);
5555
});
5656

5757
it('Object', () => {
58-
assert.deepEqual(deepExtend({ old: 123 }, {}), { old: 123 });
59-
assert.deepEqual(deepExtend({ old: 123 }, { s: 'hello' }), {
58+
expect(deepExtend({ old: 123 }, {})).to.deep.equal({ old: 123 });
59+
expect(deepExtend({ old: 123 }, { s: 'hello' })).to.deep.equal({
6060
old: 123,
6161
s: 'hello'
6262
});
63-
assert.deepEqual(
64-
deepExtend({ old: 123, a: { c: 'in-old' } }, { a: { b: 123 } }),
65-
{ old: 123, a: { b: 123, c: 'in-old' } }
66-
);
63+
expect(
64+
deepExtend({ old: 123, a: { c: 'in-old' } }, { a: { b: 123 } })
65+
).to.deep.equal({ old: 123, a: { b: 123, c: 'in-old' } });
6766
});
6867

6968
it('Array', () => {
70-
assert.deepEqual(deepExtend([1], []), []);
71-
assert.deepEqual(deepExtend([1], [123, 456]), [123, 456]);
72-
assert.deepEqual(deepExtend([1], [123, [456]]), [123, [456]]);
69+
expect(deepExtend([1], [])).to.deep.equal([]);
70+
expect(deepExtend([1], [123, 456])).to.deep.equal([123, 456]);
71+
expect(deepExtend([1], [123, [456]])).to.deep.equal([123, [456]]);
7372
});
7473

7574
it('Array is copied - not referenced', () => {
7675
const o1 = { a: [1] };
7776
const o2 = { a: [2] };
7877

79-
assert.deepEqual(deepExtend(o1, o2), { a: [2] });
78+
expect(deepExtend(o1, o2)).to.deep.equal({ a: [2] });
8079
o2.a.push(3);
81-
assert.deepEqual(o1, { a: [2] });
80+
expect(o1).to.deep.equal({ a: [2] });
8281
});
8382

8483
it('Array with undefined elements', () => {
8584
const a: any = [];
8685
a[3] = '3';
8786
const b = deepExtend(undefined, a);
88-
assert.deepEqual(b, [, , , '3']);
87+
expect(b).to.deep.equal([, , , '3']);
8988
});
9089

9190
it('Function', () => {
@@ -100,7 +99,16 @@ describe('deepExtend', () => {
10099
},
101100
{ a: source }
102101
);
103-
assert.deepEqual({ a: source }, target);
104-
assert.strictEqual(source, target.a);
102+
expect({ a: source }).to.deep.equal(target);
103+
expect(source).to.equal(target.a);
104+
});
105+
106+
it('does not extend property __proto__', () => {
107+
const src = JSON.parse('{ "__proto__": { "polluted": "polluted" } }');
108+
const a: Record<string, unknown> = {};
109+
deepExtend(a, src);
110+
111+
expect(a.__proto__).to.equal(Object.prototype);
112+
expect(a.polluted).to.be.undefined;
105113
});
106114
});

0 commit comments

Comments
 (0)