From 1f68ff76c5d1fe6d64431d51bf59a7558c9d0560 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 14 Jan 2025 20:59:12 +1100 Subject: [PATCH 1/9] fix: usernames should encode all characters in emails --- spec/EmailVerificationToken.spec.js | 33 +++++++++++++++++++++++++++++ src/Controllers/UserController.js | 5 +++-- src/Utils.js | 11 ++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index 1e9f6a7830..49d4202436 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -52,6 +52,39 @@ describe('Email Verification Token Expiration: ', () => { }); }); + it('should send an HTML or properly escaped plain text password reset email', async () => { + const user = new Parse.User(); + let sendEmailOptions; + const emailAdapter = { + sendPasswordResetEmail: (options) => { + sendEmailOptions = options; + }, + sendVerificationEmail: async () => {}, + sendMail: async () => {}, + }; + + await reconfigureServer({ + appName: 'specialCharacterUsernameTest', + publicServerURL: 'http://localhost:8378/1', + emailAdapter: emailAdapter, + }); + + user.setUsername('hello :)'); + user.setPassword('password123'); + user.set('email', 'user@parse.com'); + await user.signUp(); + + await Parse.User.requestPasswordReset('user@parse.com'); + + expect(sendEmailOptions).not.toBeUndefined(); + + const { link, html, text } = sendEmailOptions; + + const username = link.split('username=')[1]; + expect(username).toBe('%68%65%6C%6C%6F%20%3A%29'); + }); + + it('emailVerified should set to false, if the user does not verify their email before the email verify token expires', done => { const user = new Parse.User(); let sendEmailOptions; diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index ac896c51c2..0f8858ab85 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -6,6 +6,7 @@ import rest from '../rest'; import Parse from 'parse/node'; import AccountLockout from '../AccountLockout'; import Config from '../Config'; +import Utils from '../Utils'; var RestQuery = require('../RestQuery'); var Auth = require('../Auth'); @@ -173,7 +174,7 @@ export class UserController extends AdaptableController { if (!shouldSendEmail) { return; } - const username = encodeURIComponent(fetchedUser.username); + const username = Utils.encode(fetchedUser.username); const link = buildEmailLink(this.config.verifyEmailURL, username, token, this.config); const options = { @@ -286,7 +287,7 @@ export class UserController extends AdaptableController { user = await this.setPasswordResetToken(email); } const token = encodeURIComponent(user._perishable_token); - const username = encodeURIComponent(user.username); + const username = Utils.encode(user.username); const link = buildEmailLink(this.config.requestResetPasswordURL, username, token, this.config); const options = { diff --git a/src/Utils.js b/src/Utils.js index b77a3d85d7..ae5f97d891 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -399,6 +399,17 @@ class Utils { } return obj; } + + /** + * Encodes a string to be used in a URL. + * @param {String} input The string to encode. + * @returns {String} The encoded string. + */ + static encode(input) { + return Array.from(input) + .map(char => `%${char.charCodeAt(0).toString(16).padStart(2, '0').toUpperCase()}`) + .join(''); + } } module.exports = Utils; From 70cf9184b14a77655cc4d599882b368d302940a7 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 14 Jan 2025 21:04:52 +1100 Subject: [PATCH 2/9] fix lint --- spec/EmailVerificationToken.spec.js | 18 ++++++++---------- src/Utils.js | 4 ++-- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index 49d4202436..33704531e3 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -62,28 +62,26 @@ describe('Email Verification Token Expiration: ', () => { sendVerificationEmail: async () => {}, sendMail: async () => {}, }; - + await reconfigureServer({ appName: 'specialCharacterUsernameTest', publicServerURL: 'http://localhost:8378/1', emailAdapter: emailAdapter, }); - + user.setUsername('hello :)'); user.setPassword('password123'); user.set('email', 'user@parse.com'); await user.signUp(); - + await Parse.User.requestPasswordReset('user@parse.com'); - - expect(sendEmailOptions).not.toBeUndefined(); - - const { link, html, text } = sendEmailOptions; - - const username = link.split('username=')[1]; + + expect(sendEmailOptions).toBeDefined(); + + const username = sendEmailOptions.link.split('username=')[1]; expect(username).toBe('%68%65%6C%6C%6F%20%3A%29'); }); - + it('emailVerified should set to false, if the user does not verify their email before the email verify token expires', done => { const user = new Parse.User(); diff --git a/src/Utils.js b/src/Utils.js index ae5f97d891..f3afc3e282 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -407,8 +407,8 @@ class Utils { */ static encode(input) { return Array.from(input) - .map(char => `%${char.charCodeAt(0).toString(16).padStart(2, '0').toUpperCase()}`) - .join(''); + .map(char => `%${char.charCodeAt(0).toString(16).padStart(2, '0').toUpperCase()}`) + .join(''); } } From ac445f716cc6a7fa31c04bcbca0546a40635c4e6 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Jan 2025 20:48:54 +1100 Subject: [PATCH 3/9] Update EmailVerificationToken.spec.js --- spec/EmailVerificationToken.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index 33704531e3..f9510bb225 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -71,7 +71,7 @@ describe('Email Verification Token Expiration: ', () => { user.setUsername('hello :)'); user.setPassword('password123'); - user.set('email', 'user@parse.com'); + user.set('email', 'test@example.com'); await user.signUp(); await Parse.User.requestPasswordReset('user@parse.com'); From c2d4821ce0415d46bed9078ddade311050bca30a Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Jan 2025 21:36:18 +1100 Subject: [PATCH 4/9] fix tests --- spec/EmailVerificationToken.spec.js | 2 +- src/Utils.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index f9510bb225..e7022ae568 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -74,7 +74,7 @@ describe('Email Verification Token Expiration: ', () => { user.set('email', 'test@example.com'); await user.signUp(); - await Parse.User.requestPasswordReset('user@parse.com'); + await Parse.User.requestPasswordReset('test@example.com'); expect(sendEmailOptions).toBeDefined(); diff --git a/src/Utils.js b/src/Utils.js index f3afc3e282..64059f2bc9 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -406,9 +406,9 @@ class Utils { * @returns {String} The encoded string. */ static encode(input) { - return Array.from(input) - .map(char => `%${char.charCodeAt(0).toString(16).padStart(2, '0').toUpperCase()}`) - .join(''); + return encodeURIComponent(input).replace(/[!'()*]/g, char => + '%' + char.charCodeAt(0).toString(16).toUpperCase() + ); } } From 539ad9f8e4d5c61aaf2503bc9eaef3ee6a122c6f Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Jan 2025 21:42:38 +1100 Subject: [PATCH 5/9] Update EmailVerificationToken.spec.js --- spec/EmailVerificationToken.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index e7022ae568..acb7dcaf92 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -79,7 +79,7 @@ describe('Email Verification Token Expiration: ', () => { expect(sendEmailOptions).toBeDefined(); const username = sendEmailOptions.link.split('username=')[1]; - expect(username).toBe('%68%65%6C%6C%6F%20%3A%29'); + expect(username).toBe('hello%20%3A%29'); }); From 2e0d057bf4e7f5a9203078a007897bf5eb28a036 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 28 Jan 2025 09:47:33 +1100 Subject: [PATCH 6/9] expand options --- spec/EmailVerificationToken.spec.js | 13 +++++++------ src/Utils.js | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index acb7dcaf92..a29fc96d1f 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -52,7 +52,7 @@ describe('Email Verification Token Expiration: ', () => { }); }); - it('should send an HTML or properly escaped plain text password reset email', async () => { + it('should send an HTML or properly escaped plain text password reset email with all special ASCII characters', async () => { const user = new Parse.User(); let sendEmailOptions; const emailAdapter = { @@ -66,10 +66,12 @@ describe('Email Verification Token Expiration: ', () => { await reconfigureServer({ appName: 'specialCharacterUsernameTest', publicServerURL: 'http://localhost:8378/1', - emailAdapter: emailAdapter, + emailAdapter, + silent: false, }); - - user.setUsername('hello :)'); + + const specialCharacters = `!\"'),.:;<>?]^}`; + user.setUsername(specialCharacters); user.setPassword('password123'); user.set('email', 'test@example.com'); await user.signUp(); @@ -79,10 +81,9 @@ describe('Email Verification Token Expiration: ', () => { expect(sendEmailOptions).toBeDefined(); const username = sendEmailOptions.link.split('username=')[1]; - expect(username).toBe('hello%20%3A%29'); + expect(username).toBe("%21%22%27%29%2C%2E%3A%3B%3C%3E%3F%5D%5E%7D"); }); - it('emailVerified should set to false, if the user does not verify their email before the email verify token expires', done => { const user = new Parse.User(); let sendEmailOptions; diff --git a/src/Utils.js b/src/Utils.js index 64059f2bc9..b20de46677 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -406,7 +406,7 @@ class Utils { * @returns {String} The encoded string. */ static encode(input) { - return encodeURIComponent(input).replace(/[!'()*]/g, char => + return encodeURIComponent(input).replace(/[!'.()*]/g, char => '%' + char.charCodeAt(0).toString(16).toUpperCase() ); } From 42a28857b3d64ee19755596c6e0494d9eb54b1dd Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Thu, 6 Mar 2025 01:38:34 +0100 Subject: [PATCH 7/9] lint Signed-off-by: Manuel <5673677+mtrezza@users.noreply.github.com> --- src/Controllers/UserController.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 5a0d46b4db..296b7f6868 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -6,7 +6,6 @@ import rest from '../rest'; import Parse from 'parse/node'; import AccountLockout from '../AccountLockout'; import Config from '../Config'; -import Utils from '../Utils'; var RestQuery = require('../RestQuery'); var Auth = require('../Auth'); From edb8b34754bf842dec5bed9eddc9c38beb8fed26 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Thu, 6 Mar 2025 01:43:11 +0100 Subject: [PATCH 8/9] refactor --- spec/EmailVerificationToken.spec.js | 32 ----------------------------- spec/Utils.spec.js | 11 ++++++++++ src/Utils.js | 2 +- 3 files changed, 12 insertions(+), 33 deletions(-) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index a29fc96d1f..1e9f6a7830 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -52,38 +52,6 @@ describe('Email Verification Token Expiration: ', () => { }); }); - it('should send an HTML or properly escaped plain text password reset email with all special ASCII characters', async () => { - const user = new Parse.User(); - let sendEmailOptions; - const emailAdapter = { - sendPasswordResetEmail: (options) => { - sendEmailOptions = options; - }, - sendVerificationEmail: async () => {}, - sendMail: async () => {}, - }; - - await reconfigureServer({ - appName: 'specialCharacterUsernameTest', - publicServerURL: 'http://localhost:8378/1', - emailAdapter, - silent: false, - }); - - const specialCharacters = `!\"'),.:;<>?]^}`; - user.setUsername(specialCharacters); - user.setPassword('password123'); - user.set('email', 'test@example.com'); - await user.signUp(); - - await Parse.User.requestPasswordReset('test@example.com'); - - expect(sendEmailOptions).toBeDefined(); - - const username = sendEmailOptions.link.split('username=')[1]; - expect(username).toBe("%21%22%27%29%2C%2E%3A%3B%3C%3E%3F%5D%5E%7D"); - }); - it('emailVerified should set to false, if the user does not verify their email before the email verify token expires', done => { const user = new Parse.User(); let sendEmailOptions; diff --git a/spec/Utils.spec.js b/spec/Utils.spec.js index 3aa31a74b0..232a518fc5 100644 --- a/spec/Utils.spec.js +++ b/spec/Utils.spec.js @@ -1,6 +1,17 @@ const Utils = require('../src/Utils'); describe('Utils', () => { + describe('encodeForUrl', () => { + fit('should properly escape email with all special ASCII characters for use in URLs', async () => { + const values = [ + { input: `!\"'),.:;<>?]^}`, output: '%21%22%27%29%2C%2E%3A%3B%3C%3E%3F%5D%5E%7D' }, + ] + for (const value of values) { + expect(Utils.encodeForUrl(value.input)).toBe(value.output); + } + }); + }); + describe('addNestedKeysToRoot', () => { it('should move the nested keys to root of object', async () => { const obj = { diff --git a/src/Utils.js b/src/Utils.js index b20de46677..72b49aeeb2 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -405,7 +405,7 @@ class Utils { * @param {String} input The string to encode. * @returns {String} The encoded string. */ - static encode(input) { + static encodeForUrl(input) { return encodeURIComponent(input).replace(/[!'.()*]/g, char => '%' + char.charCodeAt(0).toString(16).toUpperCase() ); From 60bcbfa0b17355a6af937742decccfcb8626b0bc Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Thu, 6 Mar 2025 01:43:31 +0100 Subject: [PATCH 9/9] un-fit --- spec/Utils.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/Utils.spec.js b/spec/Utils.spec.js index 232a518fc5..fe86854e33 100644 --- a/spec/Utils.spec.js +++ b/spec/Utils.spec.js @@ -2,7 +2,7 @@ const Utils = require('../src/Utils'); describe('Utils', () => { describe('encodeForUrl', () => { - fit('should properly escape email with all special ASCII characters for use in URLs', async () => { + it('should properly escape email with all special ASCII characters for use in URLs', async () => { const values = [ { input: `!\"'),.:;<>?]^}`, output: '%21%22%27%29%2C%2E%3A%3B%3C%3E%3F%5D%5E%7D' }, ] @@ -11,7 +11,7 @@ describe('Utils', () => { } }); }); - + describe('addNestedKeysToRoot', () => { it('should move the nested keys to root of object', async () => { const obj = {