CVE-2021-23433: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (CWE-1321)
value: The package algoliasearch-helper before 3.6.2 are vulnerable to Prototype Pollution due to use of the merge function in src/SearchParameters/index.jsSearchParameters._parseNumbers without any protection against prototype properties. Note that this vulnerability is only exploitable if the implementation allows users to define arbitrary search patterns.
CWE description: The product receives input from an upstream component that specifies attributes that are to be initialized or updated in an object, but it does not properly control modifications of attributes of the object prototype.
CVSS v3 base score: 9.8
Published: 2021-11-19T20:15Z
Repository: https://github.com/algolia/algoliasearch-helper-js
Language: JavaScript
Files changed: src/functions/merge.js, test/spec/functions/defaultsPure.js, test/spec/functions/intersection.js, test/spec/functions/merge.js
Commit date: 2021-10-19 15:00:44 +0100
Commit message: fix(SearchParameters): ignore invalid parameters (#880)

* fix(SearchParameters): ignore invalid parameters

These parameters could be used maliciously, so are explicilty not allowed in merge

* clearer test
Diff: diff --git a/src/functions/merge.js b/src/functions/merge.js
index b8b9569..131256b 100644
--- a/src/functions/merge.js
+++ b/src/functions/merge.js
@@ -19,22 +19,28 @@ function _merge(target, source) {
   if (target === source) {
     return target;
   }
 
   for (var key in source) {
-    if (!Object.prototype.hasOwnProperty.call(source, key)) {
+    if (
+      !Object.prototype.hasOwnProperty.call(source, key) ||
+      key === '__proto__'
+    ) {
       continue;
     }
 
     var sourceVal = source[key];
     var targetVal = target[key];
 
     if (typeof targetVal !== 'undefined' && typeof sourceVal === 'undefined') {
       continue;
     }
 
-    if (isObjectOrArrayOrFunction(targetVal) && isObjectOrArrayOrFunction(sourceVal)) {
+    if (
+      isObjectOrArrayOrFunction(targetVal) &&
+      isObjectOrArrayOrFunction(sourceVal)
+    ) {
       target[key] = _merge(targetVal, sourceVal);
     } else {
       target[key] = clone(sourceVal);
     }
   }
diff --git a/test/spec/functions/defaultsPure.js b/test/spec/functions/defaultsPure.js
index 93233cc..8884a0c 100644
--- a/test/spec/functions/defaultsPure.js
+++ b/test/spec/functions/defaultsPure.js
@@ -90,5 +90,18 @@ it('should keep the keys order when adding facet refinements', function() {
       'facet1': ['facetValue']
     }
   );
   expect(Object.keys(actual)).toEqual(['facet1', 'facet2']);
 });
+
+it('does not pollute the prototype', () => {
+  var payload = JSON.parse('{"__proto__": {"polluted": "vulnerable to PP"}}');
+  var subject = {};
+
+  expect(subject.polluted).toBe(undefined);
+
+  const out = defaults({}, payload);
+
+  expect(out).toEqual({});
+
+  expect({}.polluted).toBe(undefined);
+});
diff --git a/test/spec/functions/intersection.js b/test/spec/functions/intersection.js
index 6d83fb0..b529e84 100644
--- a/test/spec/functions/intersection.js
+++ b/test/spec/functions/intersection.js
@@ -16,6 +16,5 @@ test('it should not produce duplicate primitive values', function() {
   expect(intersection(['0', '0', '1', '2'], ['0', '2'])).toStrictEqual([
     '0',
     '2'
   ]);
 });
-
diff --git a/test/spec/functions/merge.js b/test/spec/functions/merge.js
index e7f7015..64b92c3 100644
--- a/test/spec/functions/merge.js
+++ b/test/spec/functions/merge.js
@@ -168,5 +168,18 @@ it('should not convert strings to arrays when merging arrays of `source`', funct
   var object = {a: 'abcde'};
   var actual = merge(object, {a: ['x', 'y', 'z']});
 
   expect(actual).toStrictEqual({a: ['x', 'y', 'z']});
 });
+
+it('does not pollute the prototype', () => {
+  var payload = JSON.parse('{"__proto__": {"polluted": "vulnerable to PP"}}');
+  var subject = {};
+
+  expect(subject.polluted).toBe(undefined);
+
+  const out = merge({}, payload);
+
+  expect(out).toEqual({});
+
+  expect({}.polluted).toBe(undefined);
+});

Vulnerable code:     if (!Object.prototype.hasOwnProperty.call(source, key)) {
    if (isObjectOrArrayOrFunction(targetVal) && isObjectOrArrayOrFunction(sourceVal)) {

Fixed code:     if (
      !Object.prototype.hasOwnProperty.call(source, key) ||
      key === '__proto__'
    ) {
    if (
      isObjectOrArrayOrFunction(targetVal) &&
      isObjectOrArrayOrFunction(sourceVal)
    ) {

it('does not pollute the prototype', () => {
  var payload = JSON.parse('{"__proto__": {"polluted": "vulnerable to PP"}}');
  var subject = {};

  expect(subject.polluted).toBe(undefined);

  const out = defaults({}, payload);

  expect(out).toEqual({});

  expect({}.polluted).toBe(undefined);
});

it('does not pollute the prototype', () => {
  var payload = JSON.parse('{"__proto__": {"polluted": "vulnerable to PP"}}');
  var subject = {};

  expect(subject.polluted).toBe(undefined);

  const out = merge({}, payload);

  expect(out).toEqual({});

  expect({}.polluted).toBe(undefined);
});