CVE-2020-7765: Insufficient Information (NVD-CWE-noinfo)
value: 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.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 5.3
Published: 2020-11-16T12:15Z
Repository: https://github.com/firebase/firebase-js-sdk
Language: TypeScript
Files changed: .changeset/fuzzy-impalas-brake.md, packages/util/src/deepCopy.ts, packages/util/test/deepCopy.test.ts
Commit date: 2020-10-27 14:28:49 -0700
Commit message: Prevent __proto__ pollution in util.deepExtend (#4001)

* do not extend __proto__

* replace asserts with expects

* Create fuzzy-impalas-brake.md

* address comment
Diff: diff --git a/.changeset/fuzzy-impalas-brake.md b/.changeset/fuzzy-impalas-brake.md
new file mode 100644
index 00000000..5f171a29
--- /dev/null
+++ b/.changeset/fuzzy-impalas-brake.md
@@ -0,0 +1,5 @@
+---
+"@firebase/util": patch
+---
+
+Do not merge `__proto__` in `deepExtend` to prevent `__proto__` pollution.
diff --git a/packages/util/src/deepCopy.ts b/packages/util/src/deepCopy.ts
index 4957139e..5ef5594d 100644
--- a/packages/util/src/deepCopy.ts
+++ b/packages/util/src/deepCopy.ts
@@ -31,10 +31,12 @@ export function deepCopy<T>(value: T): T {
  * We recursively copy all child properties of plain Objects in the source- so
  * that namespace- like dictionaries are merged.
  *
  * 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)) {
     return source;
   }
@@ -60,16 +62,21 @@ export function deepExtend(target: unknown, source: unknown): unknown {
       // Not a plain Object - treat it as a scalar.
       return source;
   }
 
   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__';
+}
diff --git a/packages/util/test/deepCopy.test.ts b/packages/util/test/deepCopy.test.ts
index 267f97f3..98861ce2 100644
--- a/packages/util/test/deepCopy.test.ts
+++ b/packages/util/test/deepCopy.test.ts
@@ -12,82 +12,81 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * 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', () => {
     const source: any = (): void => {
       /*_*/
@@ -98,9 +97,18 @@ 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;
   });
 });

Vulnerable code:     if (!source.hasOwnProperty(prop)) {
    (target as { [key: string]: unknown })[prop] = deepExtend(
      (target as { [key: string]: unknown })[prop],
      (source as { [key: string]: unknown })[prop]
import { assert } from 'chai';
    assert.strictEqual(deepCopy(true), true);
    assert.strictEqual(deepCopy(123), 123);
    assert.strictEqual(deepCopy('abc'), 'abc');
    assert.deepEqual(deepCopy(d), d);
    assert.deepEqual(deepCopy({}), {});
    assert.deepEqual(deepCopy({ a: 123 }), { a: 123 });
    assert.deepEqual(deepCopy({ a: { b: 123 } }), { a: { b: 123 } });
    assert.deepEqual(deepCopy([]), []);
    assert.deepEqual(deepCopy([123, 456]), [123, 456]);
    assert.deepEqual(deepCopy([123, [456]]), [123, [456]]);
    assert.strictEqual(deepExtend(1, true), true);
    assert.strictEqual(deepExtend(undefined, 123), 123);
    assert.strictEqual(deepExtend('was', 'abc'), 'abc');
    assert.deepEqual(deepExtend(new Date(), d), d);
    assert.deepEqual(deepExtend({ old: 123 }, {}), { old: 123 });
    assert.deepEqual(deepExtend({ old: 123 }, { s: 'hello' }), {
    assert.deepEqual(
      deepExtend({ old: 123, a: { c: 'in-old' } }, { a: { b: 123 } }),
      { old: 123, a: { b: 123, c: 'in-old' } }
    );
    assert.deepEqual(deepExtend([1], []), []);
    assert.deepEqual(deepExtend([1], [123, 456]), [123, 456]);
    assert.deepEqual(deepExtend([1], [123, [456]]), [123, [456]]);
    assert.deepEqual(deepExtend(o1, o2), { a: [2] });
    assert.deepEqual(o1, { a: [2] });
    assert.deepEqual(b, [, , , '3']);
    assert.deepEqual({ a: source }, target);
    assert.strictEqual(source, target.a);
Fixed code: ---
"@firebase/util": patch
---

Do not merge `__proto__` in `deepExtend` to prevent `__proto__` pollution.
 *
 * Note: we don't merge __proto__ to prevent prototype pollution
    // use isValidKey to guard against prototype pollution. See https://snyk.io/vuln/SNYK-JS-LODASH-450202
    if (!source.hasOwnProperty(prop) || !isValidKey(prop)) {
    (target as Record<string, unknown>)[prop] = deepExtend(
      (target as Record<string, unknown>)[prop],
      (source as Record<string, unknown>)[prop]

function isValidKey(key: string): boolean {
  return key !== '__proto__';
}
import { expect } from 'chai';
    expect(deepCopy(true)).to.equal(true);
    expect(deepCopy(123)).to.equal(123);
    expect(deepCopy('abc')).to.equal('abc');
    expect(deepCopy(d)).to.deep.equal(d);
    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 } });
    expect(deepCopy([])).to.deep.equal([]);
    expect(deepCopy([123, 456])).to.deep.equal([123, 456]);
    expect(deepCopy([123, [456]])).to.deep.equal([123, [456]]);
    expect(deepExtend(1, true)).to.equal(true);
    expect(deepExtend(undefined, 123)).to.equal(123);
    expect(deepExtend('was', 'abc')).to.equal('abc');
    expect(deepExtend(new Date(), d)).to.deep.equal(d);
    expect(deepExtend({ old: 123 }, {})).to.deep.equal({ old: 123 });
    expect(deepExtend({ old: 123 }, { s: 'hello' })).to.deep.equal({
    expect(
      deepExtend({ old: 123, a: { c: 'in-old' } }, { a: { b: 123 } })
    ).to.deep.equal({ old: 123, a: { b: 123, c: 'in-old' } });
    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]]);
    expect(deepExtend(o1, o2)).to.deep.equal({ a: [2] });
    expect(o1).to.deep.equal({ a: [2] });
    expect(b).to.deep.equal([, , , '3']);
    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;