CVE-2020-26288: Cleartext Storage of Sensitive Information (CWE-312)
value: Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js. It is an npm package "parse-server". In Parse Server before version 4.5.0, user passwords involved in LDAP authentication are stored in cleartext. This is fixed in version 4.5.0 by stripping password after authentication to prevent cleartext password storage.
CWE description: The product stores sensitive information in cleartext within a resource that might be accessible to another control sphere.
CVSS v3 base score: 6.5
Published: 2020-12-30T20:15Z
Repository: https://github.com/parse-community/parse-server
Language: JavaScript
Files changed: spec/LdapAuth.spec.js, src/Adapters/Auth/ldap.js
Commit date: 2020-12-02 13:08:02 -0800
Commit message: Merge pull request from GHSA-4w46-w44m-3jq3

* strip password after authentication to prevent cleartext password storage

* fixed forgotten testcase forcing ;-/

* added test to check if password is not stored in user record

Co-authored-by: Fabian Strachanski <fabian@fastr.de>
Diff: diff --git a/spec/LdapAuth.spec.js b/spec/LdapAuth.spec.js
index 72969f39..e13c21bd 100644
--- a/spec/LdapAuth.spec.js
+++ b/spec/LdapAuth.spec.js
@@ -209,5 +209,51 @@ it('Should fail if the LDAP server encounters an error while searching', done =>
         done();
       })
       .finally(() => server.close());
   });
 });
+
+it('Should delete the password from authData after validation', done => {
+  mockLdapServer(port, 'uid=testuser, o=example', true).then(server => {
+    const options = {
+      suffix: 'o=example',
+      url: `ldap://localhost:${port}`,
+      dn: 'uid={{id}}, o=example'
+    };
+
+    const authData = { id: 'testuser', password: 'secret' };
+
+    ldap
+      .validateAuthData(authData, options)
+      .then(() => {
+        expect(authData).toEqual({ id: 'testuser' });
+        done();
+      })
+      .catch(done.fail)
+      .finally(() => server.close());
+  });
+});
+
+it('Should not save the password in the user record after authentication', done => {
+  mockLdapServer(port, 'uid=testuser, o=example', true).then(server => {
+    const options = {
+      suffix: 'o=example',
+      url: `ldap://localhost:${port}`,
+      dn: 'uid={{id}}, o=example'
+    };
+    reconfigureServer({ auth: { ldap: options } }).then(() => {
+      const authData = { authData: { id: 'testuser', password: 'secret' } };
+      Parse.User.logInWith('ldap', authData).then((returnedUser) => {
+        const query = new Parse.Query("User");
+        query
+          .equalTo('objectId', returnedUser.id).first({ useMasterKey: true })
+          .then((user) => {
+            expect(user.get('authData')).toEqual({ ldap:{ id: 'testuser' }});
+            expect(user.get('authData').ldap.password).toBeUndefined();
+            done();
+          })
+          .catch(done.fail)
+          .finally(() => server.close())
+      })
+    });
+  });
+});
diff --git a/src/Adapters/Auth/ldap.js b/src/Adapters/Auth/ldap.js
index bec35d25..a0fa637d 100644
--- a/src/Adapters/Auth/ldap.js
+++ b/src/Adapters/Auth/ldap.js
@@ -21,10 +21,11 @@ function validateAuthData(authData, options) {
       ? options.dn.replace('{{id}}', authData.id)
       : `uid=${authData.id},${options.suffix}`;
 
   return new Promise((resolve, reject) => {
     client.bind(userCn, authData.password, ldapError => {
+      delete(authData.password);
       if (ldapError) {
         let error;
         switch (ldapError.code) {
           case 49:
             error = new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'LDAP: Wrong username or password');

Fixed code: 
it('Should delete the password from authData after validation', done => {
  mockLdapServer(port, 'uid=testuser, o=example', true).then(server => {
    const options = {
      suffix: 'o=example',
      url: `ldap://localhost:${port}`,
      dn: 'uid={{id}}, o=example'
    };

    const authData = { id: 'testuser', password: 'secret' };

    ldap
      .validateAuthData(authData, options)
      .then(() => {
        expect(authData).toEqual({ id: 'testuser' });
        done();
      })
      .catch(done.fail)
      .finally(() => server.close());
  });
});

it('Should not save the password in the user record after authentication', done => {
  mockLdapServer(port, 'uid=testuser, o=example', true).then(server => {
    const options = {
      suffix: 'o=example',
      url: `ldap://localhost:${port}`,
      dn: 'uid={{id}}, o=example'
    };
    reconfigureServer({ auth: { ldap: options } }).then(() => {
      const authData = { authData: { id: 'testuser', password: 'secret' } };
      Parse.User.logInWith('ldap', authData).then((returnedUser) => {
        const query = new Parse.Query("User");
        query
          .equalTo('objectId', returnedUser.id).first({ useMasterKey: true })
          .then((user) => {
            expect(user.get('authData')).toEqual({ ldap:{ id: 'testuser' }});
            expect(user.get('authData').ldap.password).toBeUndefined();
            done();
          })
          .catch(done.fail)
          .finally(() => server.close())
      })
    });
  });
});
      delete(authData.password);