CVE-2020-5251 (published 2020-03-04T15:15Z)

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.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:P/I:N/A:N)
CVSS v3 base score: 5.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N)

Weakness information:
cwe_id: CWE-863
cwe_name: Incorrect Authorization
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.
extended_description: 
            Assuming a user with a given identity, authorization is the process of determining whether that user can access a given resource, based on the user's privileges and any permissions or other access-control specifications that apply to the resource.
            When access control checks are incorrectly applied, users are able to access data or perform actions that they should not be allowed to perform. This can lead to a wide range of problems, including information exposures, denial of service, and arbitrary code execution.
         
         

Fix information:
repo_url: https://github.com/parse-community/parse-server
commit_details: repo_url: https://github.com/parse-community/parse-server
author: Arthur Cinader
author_date: 2020-03-02 15:46:01-08:00
msg: 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>
repository: repo_url: https://github.com/parse-community/parse-server
repo_name: parse-community/parse-server
description: Parse Server for Node.js / Express
repo_language: JavaScript
owner: parse-community
file_changes: filename: RegexVulnerabilities.spec.js
change_type: ModificationType.ADD
diff: @@ -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);
+    });
+  });
+});

programming_language: TypeScript
filename: PublicAPIRouter.js
change_type: ModificationType.MODIFY
diff: @@ -11,7 +11,10 @@ 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);
 
@@ -122,7 +125,9 @@ export class PublicAPIRouter extends PromiseRouter {
       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);
@@ -158,7 +163,9 @@ export class PublicAPIRouter extends PromiseRouter {
       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);

programming_language: TypeScript
filename: middlewares.js
change_type: ModificationType.MODIFY
diff: @@ -105,6 +105,10 @@ export function handleParseHeaders(req, res, next) {
     }
   }
 
+  if (info.sessionToken && typeof info.sessionToken !== 'string') {
+    info.sessionToken = info.sessionToken.toString();
+  }
+
   if (info.clientVersion) {
     info.clientSDK = ClientSDK.fromString(info.clientVersion);
   }

programming_language: TypeScript