CVE-2020-7765 (published 2020-11-16T12:15Z)

This affects the package @firebase/util before 0.3.4. This vulnerability relates to the deepExtend function within the DeepCopy.ts file. Depending on if user input is provided, an attacker can overwrite and pollute the object prototype of a program.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:N/I:P/A:N)
CVSS v3 base score: 5.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N)

Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/firebase/firebase-js-sdk
commit_details: repo_url: https://github.com/firebase/firebase-js-sdk
author: Feiyang
author_date: 2020-10-27 14:28:49-07:00
msg: Prevent __proto__ pollution in util.deepExtend (#4001)

* do not extend __proto__

* replace asserts with expects

* Create fuzzy-impalas-brake.md

* address comment
repository: repo_url: https://github.com/firebase/firebase-js-sdk
repo_name: firebase/firebase-js-sdk
description: Firebase Javascript SDK
repo_language: TypeScript
owner: firebase
file_changes: filename: fuzzy-impalas-brake.md
change_type: ModificationType.ADD
diff: @@ -0,0 +1,5 @@
+---
+"@firebase/util": patch
+---
+
+Do not merge `__proto__` in `deepExtend` to prevent `__proto__` pollution.

programming_language: Markdown
filename: deepCopy.ts
change_type: ModificationType.MODIFY
diff: @@ -33,6 +33,8 @@ export function deepCopy<T>(value: T): T {
  *
  * Note that the target can be a function, in which case the properties in
  * the source Object are copied onto it as static properties of the Function.
+ *
+ * Note: we don't merge __proto__ to prevent prototype pollution
  */
 export function deepExtend(target: unknown, source: unknown): unknown {
   if (!(source instanceof Object)) {
@@ -62,14 +64,19 @@ export function deepExtend(target: unknown, source: unknown): unknown {
   }
 
   for (const prop in source) {
-    if (!source.hasOwnProperty(prop)) {
+    // use isValidKey to guard against prototype pollution. See https://snyk.io/vuln/SNYK-JS-LODASH-450202
+    if (!source.hasOwnProperty(prop) || !isValidKey(prop)) {
       continue;
     }
-    (target as { [key: string]: unknown })[prop] = deepExtend(
-      (target as { [key: string]: unknown })[prop],
-      (source as { [key: string]: unknown })[prop]
+    (target as Record<string, unknown>)[prop] = deepExtend(
+      (target as Record<string, unknown>)[prop],
+      (source as Record<string, unknown>)[prop]
     );
   }
 
   return target;
 }
+
+function isValidKey(key: string): boolean {
+  return key !== '__proto__';
+}

programming_language: TypeScript
filename: deepCopy.test.ts
change_type: ModificationType.MODIFY
diff: @@ -14,78 +14,77 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { assert } from 'chai';
+import { expect } from 'chai';
 import { deepCopy, deepExtend } from '../src/deepCopy';
 
 describe('deepCopy()', () => {
   it('Scalars', () => {
-    assert.strictEqual(deepCopy(true), true);
-    assert.strictEqual(deepCopy(123), 123);
-    assert.strictEqual(deepCopy('abc'), 'abc');
+    expect(deepCopy(true)).to.equal(true);
+    expect(deepCopy(123)).to.equal(123);
+    expect(deepCopy('abc')).to.equal('abc');
   });
 
   it('Date', () => {
     const d = new Date();
-    assert.deepEqual(deepCopy(d), d);
+    expect(deepCopy(d)).to.deep.equal(d);
   });
 
   it('Object', () => {
-    assert.deepEqual(deepCopy({}), {});
-    assert.deepEqual(deepCopy({ a: 123 }), { a: 123 });
-    assert.deepEqual(deepCopy({ a: { b: 123 } }), { a: { b: 123 } });
+    expect(deepCopy({})).to.deep.equal({});
+    expect(deepCopy({ a: 123 })).to.deep.equal({ a: 123 });
+    expect(deepCopy({ a: { b: 123 } })).to.deep.equal({ a: { b: 123 } });
   });
 
   it('Array', () => {
-    assert.deepEqual(deepCopy([]), []);
-    assert.deepEqual(deepCopy([123, 456]), [123, 456]);
-    assert.deepEqual(deepCopy([123, [456]]), [123, [456]]);
+    expect(deepCopy([])).to.deep.equal([]);
+    expect(deepCopy([123, 456])).to.deep.equal([123, 456]);
+    expect(deepCopy([123, [456]])).to.deep.equal([123, [456]]);
   });
 });
 
 describe('deepExtend', () => {
   it('Scalars', () => {
-    assert.strictEqual(deepExtend(1, true), true);
-    assert.strictEqual(deepExtend(undefined, 123), 123);
-    assert.strictEqual(deepExtend('was', 'abc'), 'abc');
+    expect(deepExtend(1, true)).to.equal(true);
+    expect(deepExtend(undefined, 123)).to.equal(123);
+    expect(deepExtend('was', 'abc')).to.equal('abc');
   });
 
   it('Date', () => {
     const d = new Date();
-    assert.deepEqual(deepExtend(new Date(), d), d);
+    expect(deepExtend(new Date(), d)).to.deep.equal(d);
   });
 
   it('Object', () => {
-    assert.deepEqual(deepExtend({ old: 123 }, {}), { old: 123 });
-    assert.deepEqual(deepExtend({ old: 123 }, { s: 'hello' }), {
+    expect(deepExtend({ old: 123 }, {})).to.deep.equal({ old: 123 });
+    expect(deepExtend({ old: 123 }, { s: 'hello' })).to.deep.equal({
       old: 123,
       s: 'hello'
     });
-    assert.deepEqual(
-      deepExtend({ old: 123, a: { c: 'in-old' } }, { a: { b: 123 } }),
-      { old: 123, a: { b: 123, c: 'in-old' } }
-    );
+    expect(
+      deepExtend({ old: 123, a: { c: 'in-old' } }, { a: { b: 123 } })
+    ).to.deep.equal({ old: 123, a: { b: 123, c: 'in-old' } });
   });
 
   it('Array', () => {
-    assert.deepEqual(deepExtend([1], []), []);
-    assert.deepEqual(deepExtend([1], [123, 456]), [123, 456]);
-    assert.deepEqual(deepExtend([1], [123, [456]]), [123, [456]]);
+    expect(deepExtend([1], [])).to.deep.equal([]);
+    expect(deepExtend([1], [123, 456])).to.deep.equal([123, 456]);
+    expect(deepExtend([1], [123, [456]])).to.deep.equal([123, [456]]);
   });
 
   it('Array is copied - not referenced', () => {
     const o1 = { a: [1] };
     const o2 = { a: [2] };
 
-    assert.deepEqual(deepExtend(o1, o2), { a: [2] });
+    expect(deepExtend(o1, o2)).to.deep.equal({ a: [2] });
     o2.a.push(3);
-    assert.deepEqual(o1, { a: [2] });
+    expect(o1).to.deep.equal({ a: [2] });
   });
 
   it('Array with undefined elements', () => {
     const a: any = [];
     a[3] = '3';
     const b = deepExtend(undefined, a);
-    assert.deepEqual(b, [, , , '3']);
+    expect(b).to.deep.equal([, , , '3']);
   });
 
   it('Function', () => {
@@ -100,7 +99,16 @@ describe('deepExtend', () => {
       },
       { a: source }
     );
-    assert.deepEqual({ a: source }, target);
-    assert.strictEqual(source, target.a);
+    expect({ a: source }).to.deep.equal(target);
+    expect(source).to.equal(target.a);
+  });
+
+  it('does not extend property __proto__', () => {
+    const src = JSON.parse('{ "__proto__": { "polluted": "polluted" } }');
+    const a: Record<string, unknown> = {};
+    deepExtend(a, src);
+
+    expect(a.__proto__).to.equal(Object.prototype);
+    expect(a.polluted).to.be.undefined;
   });
 });

programming_language: TypeScript