From 3a3a5eee5ffa48da1352423312cb767de14de269 Mon Sep 17 00:00:00 2001 From: Arthur Cinader <700572+acinader@users.noreply.github.com> Date: Mon, 2 Mar 2020 15:46:01 -0800 Subject: [PATCH] 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 --- spec/RegexVulnerabilities.spec.js | 198 ++++++++++++++++++++++++++++++ src/Routers/PublicAPIRouter.js | 13 +- src/middlewares.js | 4 + 3 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 spec/RegexVulnerabilities.spec.js 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 @@ -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); diff --git a/src/middlewares.js b/src/middlewares.js index 75923e71..f60cb53d 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -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); }