CVE-2020-5251: Incorrect Authorization (CWE-863)
value: In parser-server before version 4.1.0, you can fetch all the users objects, by using regex in the NoSQL query. Using the NoSQL, you can use a regex on sessionToken and find valid accounts this way.
CWE description: The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
CVSS v3 base score: 5.3
Published: 2020-03-04T15:15Z
Repository: https://github.com/parse-community/parse-server
Language: JavaScript
Files changed: spec/RegexVulnerabilities.spec.js, src/Routers/PublicAPIRouter.js, src/middlewares.js
Commit date: 2020-03-02 15:46:01 -0800
Commit message: Merge pull request from GHSA-h4mf-75hf-67w4

* Fix session token issue

* verify email problem

* Fix password reset problem

* Change test file name

* Split tests

* Refetch user

* Replaces lets to consts

* Refactor unit test

What you have is just finee, but wanted
to show you what I meant with my comment

Use jasmine's this to set stuff in beforeEach's

Not that all functions need to be `function ()` instead of
`() =>` so `this` is preserved.

see: https://jasmine.github.io/tutorials/your_first_suite#section-The_%3Ccode%3Ethis%3C/code%3E_keyword

Co-authored-by: Antonio Davi Macedo Coelho de Castro <adavimacedo@gmail.com>

Diff: diff --git a/spec/RegexVulnerabilities.spec.js b/spec/RegexVulnerabilities.spec.js
new file mode 100644
index 00000000..58f5afac
--- /dev/null
+++ b/spec/RegexVulnerabilities.spec.js
@@ -0,0 +1,198 @@
+const request = require('../lib/request');
+
+const serverURL = 'http://localhost:8378/1';
+const headers = {
+  'Content-Type': 'application/json',
+};
+const keys = {
+  _ApplicationId: 'test',
+  _JavaScriptKey: 'test',
+};
+const emailAdapter = {
+  sendVerificationEmail: () => Promise.resolve(),
+  sendPasswordResetEmail: () => Promise.resolve(),
+  sendMail: () => {},
+};
+const appName = 'test';
+const publicServerURL = 'http://localhost:8378/1';
+
+describe('Regex Vulnerabilities', function() {
+  beforeEach(async function() {
+    await reconfigureServer({
+      verifyUserEmails: true,
+      emailAdapter,
+      appName,
+      publicServerURL,
+    });
+
+    const signUpResponse = await request({
+      url: `${serverURL}/users`,
+      method: 'POST',
+      headers,
+      body: JSON.stringify({
+        ...keys,
+        _method: 'POST',
+        username: 'someemail@somedomain.com',
+        password: 'somepassword',
+        email: 'someemail@somedomain.com',
+      }),
+    });
+    this.objectId = signUpResponse.data.objectId;
+    this.sessionToken = signUpResponse.data.sessionToken;
+    this.partialSessionToken = this.sessionToken.slice(0, 3);
+  });
+
+  describe('on session token', function() {
+    it('should not work with regex', async function() {
+      try {
+        await request({
+          url: `${serverURL}/users/me`,
+          method: 'POST',
+          headers,
+          body: JSON.stringify({
+            ...keys,
+            _SessionToken: {
+              $regex: this.partialSessionToken,
+            },
+            _method: 'GET',
+          }),
+        });
+        fail('should not work');
+      } catch (e) {
+        expect(e.data.code).toEqual(209);
+        expect(e.data.error).toEqual('Invalid session token');
+      }
+    });
+
+    it('should work with plain token', async function() {
+      const meResponse = await request({
+        url: `${serverURL}/users/me`,
+        method: 'POST',
+        headers,
+        body: JSON.stringify({
+          ...keys,
+          _SessionToken: this.sessionToken,
+          _method: 'GET',
+        }),
+      });
+      expect(meResponse.data.objectId).toEqual(this.objectId);
+      expect(meResponse.data.sessionToken).toEqual(this.sessionToken);
+    });
+  });
+
+  describe('on verify e-mail', function() {
+    beforeEach(async function() {
+      const userQuery = new Parse.Query(Parse.User);
+      this.user = await userQuery.get(this.objectId, { useMasterKey: true });
+    });
+
+    it('should not work with regex', async function() {
+      expect(this.user.get('emailVerified')).toEqual(false);
+      await request({
+        url: `${serverURL}/apps/test/verify_email?username=someemail@somedomain.com&token[$regex]=`,
+        method: 'GET',
+      });
+      await this.user.fetch({ useMasterKey: true });
+      expect(this.user.get('emailVerified')).toEqual(false);
+    });
+
+    it('should work with plain token', async function() {
+      expect(this.user.get('emailVerified')).toEqual(false);
+      // It should work
+      await request({
+        url: `${serverURL}/apps/test/verify_email?username=someemail@somedomain.com&token=${this.user.get(
+          '_email_verify_token'
+        )}`,
+        method: 'GET',
+      });
+      await this.user.fetch({ useMasterKey: true });
+      expect(this.user.get('emailVerified')).toEqual(true);
+    });
+  });
+
+  describe('on password reset', function() {
+    beforeEach(async function() {
+      this.user = await Parse.User.logIn(
+        'someemail@somedomain.com',
+        'somepassword'
+      );
+    });
+
+    it('should not work with regex', async function() {
+      expect(this.user.id).toEqual(this.objectId);
+      await request({
+        url: `${serverURL}/requestPasswordReset`,
+        method: 'POST',
+        headers,
+        body: JSON.stringify({
+          ...keys,
+          _method: 'POST',
+          email: 'someemail@somedomain.com',
+        }),
+      });
+      await this.user.fetch({ useMasterKey: true });
+      const passwordResetResponse = await request({
+        url: `${serverURL}/apps/test/request_password_reset?username=someemail@somedomain.com&token[$regex]=`,
+        method: 'GET',
+      });
+      expect(passwordResetResponse.status).toEqual(302);
+      expect(passwordResetResponse.headers.location).toMatch(
+        `\\/invalid\\_link\\.html`
+      );
+      await request({
+        url: `${serverURL}/apps/test/request_password_reset`,
+        method: 'POST',
+        body: {
+          token: { $regex: '' },
+          username: 'someemail@somedomain.com',
+          new_password: 'newpassword',
+        },
+      });
+      try {
+        await Parse.User.logIn('someemail@somedomain.com', 'newpassword');
+        fail('should not work');
+      } catch (e) {
+        expect(e.code).toEqual(101);
+        expect(e.message).toEqual('Invalid username/password.');
+      }
+    });
+
+    it('should work with plain token', async function() {
+      expect(this.user.id).toEqual(this.objectId);
+      await request({
+        url: `${serverURL}/requestPasswordReset`,
+        method: 'POST',
+        headers,
+        body: JSON.stringify({
+          ...keys,
+          _method: 'POST',
+          email: 'someemail@somedomain.com',
+        }),
+      });
+      await this.user.fetch({ useMasterKey: true });
+      const token = this.user.get('_perishable_token');
+      const passwordResetResponse = await request({
+        url: `${serverURL}/apps/test/request_password_reset?username=someemail@somedomain.com&token=${token}`,
+        method: 'GET',
+      });
+      expect(passwordResetResponse.status).toEqual(302);
+      expect(passwordResetResponse.headers.location).toMatch(
+        `\\/choose\\_password\\?token\\=${token}\\&`
+      );
+      await request({
+        url: `${serverURL}/apps/test/request_password_reset`,
+        method: 'POST',
+        body: {
+          token,
+          username: 'someemail@somedomain.com',
+          new_password: 'newpassword',
+        },
+      });
+      const userAgain = await Parse.User.logIn(
+        'someemail@somedomain.com',
+        'newpassword'
+      );
+      expect(userAgain.id).toEqual(this.objectId);
+    });
+  });
+});
diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js
index f30c5289..74538354 100644
--- a/src/Routers/PublicAPIRouter.js
+++ b/src/Routers/PublicAPIRouter.js
@@ -9,11 +9,14 @@ import { Parse } from 'parse/node';
 const public_html = path.resolve(__dirname, '../../public_html');
 const views = path.resolve(__dirname, '../../views');
 
 export class PublicAPIRouter extends PromiseRouter {
   verifyEmail(req) {
-    const { token, username } = req.query;
+    const { username, token: rawToken } = req.query;
+    const token =
+      rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken;
+
     const appId = req.params.appId;
     const config = Config.get(appId);
 
     if (!config) {
       this.invalidRequest();
@@ -120,11 +123,13 @@ export class PublicAPIRouter extends PromiseRouter {
 
     if (!config.publicServerURL) {
       return this.missingPublicServerURL();
     }
 
-    const { username, token } = req.query;
+    const { username, token: rawToken } = req.query;
+    const token =
+      rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken;
 
     if (!username || !token) {
       return this.invalidLink(req);
     }
 
@@ -156,11 +161,13 @@ export class PublicAPIRouter extends PromiseRouter {
 
     if (!config.publicServerURL) {
       return this.missingPublicServerURL();
     }
 
-    const { username, token, new_password } = req.body;
+    const { username, new_password, token: rawToken } = req.body;
+    const token =
+      rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken;
 
     if ((!username || !token || !new_password) && req.xhr === false) {
       return this.invalidLink(req);
     }
 
diff --git a/src/middlewares.js b/src/middlewares.js
index 75923e71..f60cb53d 100644
--- a/src/middlewares.js
+++ b/src/middlewares.js
@@ -103,10 +103,14 @@ export function handleParseHeaders(req, res, next) {
     } else {
       return invalidRequest(req, res);
     }
   }
 
+  if (info.sessionToken && typeof info.sessionToken !== 'string') {
+    info.sessionToken = info.sessionToken.toString();
+  }
+
   if (info.clientVersion) {
     info.clientSDK = ClientSDK.fromString(info.clientVersion);
   }
 
   if (fileViaJSON) {

Vulnerable code:     const { token, username } = req.query;
    const { username, token } = req.query;
    const { username, token, new_password } = req.body;
Fixed code: const request = require('../lib/request');

const serverURL = 'http://localhost:8378/1';
const headers = {
  'Content-Type': 'application/json',
};
const keys = {
  _ApplicationId: 'test',
  _JavaScriptKey: 'test',
};
const emailAdapter = {
  sendVerificationEmail: () => Promise.resolve(),
  sendPasswordResetEmail: () => Promise.resolve(),
  sendMail: () => {},
};
const appName = 'test';
const publicServerURL = 'http://localhost:8378/1';

describe('Regex Vulnerabilities', function() {
  beforeEach(async function() {
    await reconfigureServer({
      verifyUserEmails: true,
      emailAdapter,
      appName,
      publicServerURL,
    });

    const signUpResponse = await request({
      url: `${serverURL}/users`,
      method: 'POST',
      headers,
      body: JSON.stringify({
        ...keys,
        _method: 'POST',
        username: 'someemail@somedomain.com',
        password: 'somepassword',
        email: 'someemail@somedomain.com',
      }),
    });
    this.objectId = signUpResponse.data.objectId;
    this.sessionToken = signUpResponse.data.sessionToken;
    this.partialSessionToken = this.sessionToken.slice(0, 3);
  });

  describe('on session token', function() {
    it('should not work with regex', async function() {
      try {
        await request({
          url: `${serverURL}/users/me`,
          method: 'POST',
          headers,
          body: JSON.stringify({
            ...keys,
            _SessionToken: {
              $regex: this.partialSessionToken,
            },
            _method: 'GET',
          }),
        });
        fail('should not work');
      } catch (e) {
        expect(e.data.code).toEqual(209);
        expect(e.data.error).toEqual('Invalid session token');
      }
    });

    it('should work with plain token', async function() {
      const meResponse = await request({
        url: `${serverURL}/users/me`,
        method: 'POST',
        headers,
        body: JSON.stringify({
          ...keys,
          _SessionToken: this.sessionToken,
          _method: 'GET',
        }),
      });
      expect(meResponse.data.objectId).toEqual(this.objectId);
      expect(meResponse.data.sessionToken).toEqual(this.sessionToken);
    });
  });

  describe('on verify e-mail', function() {
    beforeEach(async function() {
      const userQuery = new Parse.Query(Parse.User);
      this.user = await userQuery.get(this.objectId, { useMasterKey: true });
    });

    it('should not work with regex', async function() {
      expect(this.user.get('emailVerified')).toEqual(false);
      await request({
        url: `${serverURL}/apps/test/verify_email?username=someemail@somedomain.com&token[$regex]=`,
        method: 'GET',
      });
      await this.user.fetch({ useMasterKey: true });
      expect(this.user.get('emailVerified')).toEqual(false);
    });

    it('should work with plain token', async function() {
      expect(this.user.get('emailVerified')).toEqual(false);
      // It should work
      await request({
        url: `${serverURL}/apps/test/verify_email?username=someemail@somedomain.com&token=${this.user.get(
          '_email_verify_token'
        )}`,
        method: 'GET',
      });
      await this.user.fetch({ useMasterKey: true });
      expect(this.user.get('emailVerified')).toEqual(true);
    });
  });

  describe('on password reset', function() {
    beforeEach(async function() {
      this.user = await Parse.User.logIn(
        'someemail@somedomain.com',
        'somepassword'
      );
    });

    it('should not work with regex', async function() {
      expect(this.user.id).toEqual(this.objectId);
      await request({
        url: `${serverURL}/requestPasswordReset`,
        method: 'POST',
        headers,
        body: JSON.stringify({
          ...keys,
          _method: 'POST',
          email: 'someemail@somedomain.com',
        }),
      });
      await this.user.fetch({ useMasterKey: true });
      const passwordResetResponse = await request({
        url: `${serverURL}/apps/test/request_password_reset?username=someemail@somedomain.com&token[$regex]=`,
        method: 'GET',
      });
      expect(passwordResetResponse.status).toEqual(302);
      expect(passwordResetResponse.headers.location).toMatch(
        `\\/invalid\\_link\\.html`
      );
      await request({
        url: `${serverURL}/apps/test/request_password_reset`,
        method: 'POST',
        body: {
          token: { $regex: '' },
          username: 'someemail@somedomain.com',
          new_password: 'newpassword',
        },
      });
      try {
        await Parse.User.logIn('someemail@somedomain.com', 'newpassword');
        fail('should not work');
      } catch (e) {
        expect(e.code).toEqual(101);
        expect(e.message).toEqual('Invalid username/password.');
      }
    });

    it('should work with plain token', async function() {
      expect(this.user.id).toEqual(this.objectId);
      await request({
        url: `${serverURL}/requestPasswordReset`,
        method: 'POST',
        headers,
        body: JSON.stringify({
          ...keys,
          _method: 'POST',
          email: 'someemail@somedomain.com',
        }),
      });
      await this.user.fetch({ useMasterKey: true });
      const token = this.user.get('_perishable_token');
      const passwordResetResponse = await request({
        url: `${serverURL}/apps/test/request_password_reset?username=someemail@somedomain.com&token=${token}`,
        method: 'GET',
      });
      expect(passwordResetResponse.status).toEqual(302);
      expect(passwordResetResponse.headers.location).toMatch(
        `\\/choose\\_password\\?token\\=${token}\\&`
      );
      await request({
        url: `${serverURL}/apps/test/request_password_reset`,
        method: 'POST',
        body: {
          token,
          username: 'someemail@somedomain.com',
          new_password: 'newpassword',
        },
      });
      const userAgain = await Parse.User.logIn(
        'someemail@somedomain.com',
        'newpassword'
      );
      expect(userAgain.id).toEqual(this.objectId);
    });
  });
});
    const { username, token: rawToken } = req.query;
    const token =
      rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken;

    const { username, token: rawToken } = req.query;
    const token =
      rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken;
    const { username, new_password, token: rawToken } = req.body;
    const token =
      rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken;
  if (info.sessionToken && typeof info.sessionToken !== 'string') {
    info.sessionToken = info.sessionToken.toString();
  }