CVE-2020-5263: Insufficiently Protected Credentials (CWE-522)
value: auth0.js (NPM package auth0-js) greater than version 8.0.0 and before version 9.12.3 has a vulnerability. In the case of an (authentication) error, the error object returned by the library contains the original request of the user, which may include the plaintext password the user entered. If the error object is exposed or logged without modification, the application risks password exposure. This is fixed in version 9.12.3
CWE description: The product transmits or stores authentication credentials, but it uses an insecure method that is susceptible to unauthorized interception and/or retrieval.
CVSS v3 base score: 4.9
Published: 2020-04-09T16:15Z
Repository: https://github.com/auth0/auth0.js
Language: JavaScript
Files changed: src/helper/object.js, src/helper/response-handler.js, test/helper/object.test.js, test/helper/response-handler.test.js
Commit date: 2020-04-09 09:22:45 +0100
Commit message: Merge pull request from GHSA-prfq-f66g-43mp

* Applied #1085 from auth0.js (fixes broken tests)

* Added an object helper for setting properties at a deep level

* Masking password in original error object
Diff: diff --git a/src/helper/object.js b/src/helper/object.js
index 63c5a15..fddf033 100644
--- a/src/helper/object.js
+++ b/src/helper/object.js
@@ -166,17 +166,40 @@ function trimMultiple(options, keys) {
 
 function trimUserDetails(options) {
   return trimMultiple(options, ['username', 'email', 'phoneNumber']);
 }
 
+/**
+ * Updates the value of a property on the given object, using a deep path selector.
+ * @param {object} obj The object to set the property value on
+ * @param {string|array} path The path to the property that should have its value updated. e.g. 'prop1.prop2.prop3' or ['prop1', 'prop2', 'prop3']
+ * @param {any} value The value to set
+ */
+function updatePropertyOn(obj, path, value) {
+  if (typeof path === 'string') {
+    path = path.split('.');
+  }
+
+  var next = path[0];
+
+  if (obj.hasOwnProperty(next)) {
+    if (path.length === 1) {
+      obj[next] = value;
+    } else {
+      updatePropertyOn(obj[next], path.slice(1), value);
+    }
+  }
+}
+
 export default {
   toSnakeCase: toSnakeCase,
   toCamelCase: toCamelCase,
   blacklist: blacklist,
   merge: merge,
   pick: pick,
   getKeysNotIn: getKeysNotIn,
   extend: extend,
   getOriginFromUrl: getOriginFromUrl,
   getLocationFromUrl: getLocationFromUrl,
-  trimUserDetails: trimUserDetails
+  trimUserDetails: trimUserDetails,
+  updatePropertyOn: updatePropertyOn
 };
diff --git a/src/helper/response-handler.js b/src/helper/response-handler.js
index 15e754c..edb1ec1 100644
--- a/src/helper/response-handler.js
+++ b/src/helper/response-handler.js
@@ -25,10 +25,16 @@ function wrapCallback(cb, options) {
     if (err) {
       errObj = {
         original: err
       };
 
+      objectHelper.updatePropertyOn(
+        errObj,
+        'original.response.req._data.password',
+        '*****'
+      );
+
       if (err.response && err.response.statusCode) {
         errObj.statusCode = err.response.statusCode;
       }
 
       if (err.response && err.response.statusText) {
diff --git a/test/helper/object.test.js b/test/helper/object.test.js
index 54285f5..b70e545 100644
--- a/test/helper/object.test.js
+++ b/test/helper/object.test.js
@@ -705,6 +705,64 @@ describe('helpers', function() {
         attr2: 'attribute_2',
         phoneNumber: '+16505555555'
       });
     });
   });
+
+  describe('setPropertyValue', function() {
+    it('can set a property at the first level of the object', function() {
+      var obj = {
+        one: 1,
+        two: 2,
+        three: 3
+      };
+
+      objectHelper.updatePropertyOn(obj, 'one', 'one');
+
+      expect(obj).to.eql({
+        one: 'one',
+        two: 2,
+        three: 3
+      });
+    });
+
+    it('can set a nested property', function() {
+      var obj = {
+        one: {
+          two: {
+            three: 3
+          }
+        }
+      };
+
+      objectHelper.updatePropertyOn(obj, 'one.two.three', 'three');
+
+      expect(obj).to.eql({
+        one: {
+          two: {
+            three: 'three'
+          }
+        }
+      });
+    });
+
+    it("does not add new values if the key doesn't already exist", function() {
+      var obj = {
+        one: {
+          two: {
+            three: 3
+          }
+        }
+      };
+
+      objectHelper.updatePropertyOn(obj, 'one.two.four', 4);
+
+      expect(obj).to.eql({
+        one: {
+          two: {
+            three: 3
+          }
+        }
+      });
+    });
+  });
 });
diff --git a/test/helper/response-handler.test.js b/test/helper/response-handler.test.js
index 499510e..1c6f2f9 100644
--- a/test/helper/response-handler.test.js
+++ b/test/helper/response-handler.test.js
@@ -239,6 +239,90 @@ describe('helpers responseHandler', function() {
         done();
       },
       { keepOriginalCasing: true }
     )(null, assert_data);
   });
+
+  it('should mask the password object in the original response object', function(done) {
+    var assert_err = {
+      code: 'the_error_code',
+      error: 'The error description.',
+      response: {
+        req: {
+          _data: {
+            realm: 'realm',
+            client_id: 'client_id',
+            username: 'username',
+            password: 'this is a password'
+          }
+        }
+      }
+    };
+
+    responseHandler(function(err, data) {
+      expect(data).to.be(undefined);
+
+      expect(err).to.eql({
+        original: {
+          code: 'the_error_code',
+          error: 'The error description.',
+          response: {
+            req: {
+              _data: {
+                realm: 'realm',
+                client_id: 'client_id',
+                username: 'username',
+                password: '*****'
+              }
+            }
+          }
+        },
+        code: 'the_error_code',
+        description: 'The error description.'
+      });
+
+      done();
+    })(assert_err, null);
+  });
+
+  it('should mask the password object in the data object', function(done) {
+    var assert_err = {
+      code: 'the_error_code',
+      error: 'The error description.',
+      response: {
+        req: {
+          _data: {
+            realm: 'realm',
+            client_id: 'client_id',
+            username: 'username',
+            password: 'this is a password'
+          }
+        }
+      }
+    };
+
+    responseHandler(function(err, data) {
+      expect(data).to.be(undefined);
+
+      expect(err).to.eql({
+        original: {
+          code: 'the_error_code',
+          error: 'The error description.',
+          response: {
+            req: {
+              _data: {
+                realm: 'realm',
+                client_id: 'client_id',
+                username: 'username',
+                password: '*****'
+              }
+            }
+          }
+        },
+        code: 'the_error_code',
+        description: 'The error description.'
+      });
+
+      done();
+    })(assert_err, null);
+  });
 });

Vulnerable code:   trimUserDetails: trimUserDetails
Fixed code: /**
 * Updates the value of a property on the given object, using a deep path selector.
 * @param {object} obj The object to set the property value on
 * @param {string|array} path The path to the property that should have its value updated. e.g. 'prop1.prop2.prop3' or ['prop1', 'prop2', 'prop3']
 * @param {any} value The value to set
 */
function updatePropertyOn(obj, path, value) {
  if (typeof path === 'string') {
    path = path.split('.');
  }

  var next = path[0];

  if (obj.hasOwnProperty(next)) {
    if (path.length === 1) {
      obj[next] = value;
    } else {
      updatePropertyOn(obj[next], path.slice(1), value);
    }
  }
}

  trimUserDetails: trimUserDetails,
  updatePropertyOn: updatePropertyOn
      objectHelper.updatePropertyOn(
        errObj,
        'original.response.req._data.password',
        '*****'
      );


  describe('setPropertyValue', function() {
    it('can set a property at the first level of the object', function() {
      var obj = {
        one: 1,
        two: 2,
        three: 3
      };

      objectHelper.updatePropertyOn(obj, 'one', 'one');

      expect(obj).to.eql({
        one: 'one',
        two: 2,
        three: 3
      });
    });

    it('can set a nested property', function() {
      var obj = {
        one: {
          two: {
            three: 3
          }
        }
      };

      objectHelper.updatePropertyOn(obj, 'one.two.three', 'three');

      expect(obj).to.eql({
        one: {
          two: {
            three: 'three'
          }
        }
      });
    });

    it("does not add new values if the key doesn't already exist", function() {
      var obj = {
        one: {
          two: {
            three: 3
          }
        }
      };

      objectHelper.updatePropertyOn(obj, 'one.two.four', 4);

      expect(obj).to.eql({
        one: {
          two: {
            three: 3
          }
        }
      });
    });
  });

  it('should mask the password object in the original response object', function(done) {
    var assert_err = {
      code: 'the_error_code',
      error: 'The error description.',
      response: {
        req: {
          _data: {
            realm: 'realm',
            client_id: 'client_id',
            username: 'username',
            password: 'this is a password'
          }
        }
      }
    };

    responseHandler(function(err, data) {
      expect(data).to.be(undefined);

      expect(err).to.eql({
        original: {
          code: 'the_error_code',
          error: 'The error description.',
          response: {
            req: {
              _data: {
                realm: 'realm',
                client_id: 'client_id',
                username: 'username',
                password: '*****'
              }
            }
          }
        },
        code: 'the_error_code',
        description: 'The error description.'
      });

      done();
    })(assert_err, null);
  });

  it('should mask the password object in the data object', function(done) {
    var assert_err = {
      code: 'the_error_code',
      error: 'The error description.',
      response: {
        req: {
          _data: {
            realm: 'realm',
            client_id: 'client_id',
            username: 'username',
            password: 'this is a password'
          }
        }
      }
    };

    responseHandler(function(err, data) {
      expect(data).to.be(undefined);

      expect(err).to.eql({
        original: {
          code: 'the_error_code',
          error: 'The error description.',
          response: {
            req: {
              _data: {
                realm: 'realm',
                client_id: 'client_id',
                username: 'username',
                password: '*****'
              }
            }
          }
        },
        code: 'the_error_code',
        description: 'The error description.'
      });

      done();
    })(assert_err, null);
  });