From 3bf18e09edabb011a742f4f728e1cfb2c3112032 Mon Sep 17 00:00:00 2001 From: Kalista Payne Date: Tue, 16 Dec 2025 16:17:19 -0600 Subject: [PATCH 1/8] fix(auth): strip invalid characters during social reg --- website/server/libs/auth/social.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/website/server/libs/auth/social.js b/website/server/libs/auth/social.js index a6101378d5..7758b39f1e 100644 --- a/website/server/libs/auth/social.js +++ b/website/server/libs/auth/social.js @@ -98,6 +98,11 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre throw new NotFound(res.t('userNotFound')); } + sanitizedUsername = username.replace(/[^a-zA-Z0-9_-]/g, ''); + if (!sanitizedUsername) { + sanitizedUsername = generateUsername(); + } + if (existingUser) { existingUser.auth[network] = { id: profile.id, @@ -112,8 +117,8 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre emails: profile.emails, }, local: { - username, - lowerCaseUsername: username.toLowerCase(), + username: sanitizedUsername, + lowerCaseUsername: sanitizedUsername.toLowerCase(), email, }, }, From 72fb41c7e06db1b65116630dd95689ffeaa32a87 Mon Sep 17 00:00:00 2001 From: Kalista Payne Date: Tue, 16 Dec 2025 16:21:50 -0600 Subject: [PATCH 2/8] fix(lint): missing variable declaration --- website/server/libs/auth/social.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/server/libs/auth/social.js b/website/server/libs/auth/social.js index 7758b39f1e..d234ec632e 100644 --- a/website/server/libs/auth/social.js +++ b/website/server/libs/auth/social.js @@ -98,7 +98,7 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre throw new NotFound(res.t('userNotFound')); } - sanitizedUsername = username.replace(/[^a-zA-Z0-9_-]/g, ''); + let sanitizedUsername = username.replace(/[^a-zA-Z0-9_-]/g, ''); if (!sanitizedUsername) { sanitizedUsername = generateUsername(); } From 43808696a8d3c8e9eae36f208108a2d6751f5e60 Mon Sep 17 00:00:00 2001 From: Kalista Payne Date: Wed, 7 Jan 2026 14:11:29 -0600 Subject: [PATCH 3/8] fix(lint): whitespace; also revert username fix for further QA --- website/server/libs/auth/social.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/website/server/libs/auth/social.js b/website/server/libs/auth/social.js index d234ec632e..a6101378d5 100644 --- a/website/server/libs/auth/social.js +++ b/website/server/libs/auth/social.js @@ -98,11 +98,6 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre throw new NotFound(res.t('userNotFound')); } - let sanitizedUsername = username.replace(/[^a-zA-Z0-9_-]/g, ''); - if (!sanitizedUsername) { - sanitizedUsername = generateUsername(); - } - if (existingUser) { existingUser.auth[network] = { id: profile.id, @@ -117,8 +112,8 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre emails: profile.emails, }, local: { - username: sanitizedUsername, - lowerCaseUsername: sanitizedUsername.toLowerCase(), + username, + lowerCaseUsername: username.toLowerCase(), email, }, }, From 7559feec8edaeeafbbdc7a224d765fa089f1a118 Mon Sep 17 00:00:00 2001 From: Kalista Payne Date: Wed, 7 Jan 2026 14:14:20 -0600 Subject: [PATCH 4/8] Revert "fix(lint): whitespace; also revert username fix for further QA" This reverts commit dcd15a58ebf41d9e44c89c675419ecb7d36b946d. --- website/client/src/components/appFooter.vue | 4 +++- website/server/libs/auth/social.js | 9 +++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/website/client/src/components/appFooter.vue b/website/client/src/components/appFooter.vue index b7823a1a78..9720cdd866 100644 --- a/website/client/src/components/appFooter.vue +++ b/website/client/src/components/appFooter.vue @@ -303,6 +303,7 @@ > +
+30 Days
+
Date: Wed, 7 Jan 2026 14:14:51 -0600 Subject: [PATCH 5/8] fix(lint): whitespace --- website/client/src/components/appFooter.vue | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/website/client/src/components/appFooter.vue b/website/client/src/components/appFooter.vue index 9720cdd866..b7823a1a78 100644 --- a/website/client/src/components/appFooter.vue +++ b/website/client/src/components/appFooter.vue @@ -303,7 +303,6 @@ >
-
+30 Days
-
Date: Wed, 7 Jan 2026 17:26:42 -0600 Subject: [PATCH 6/8] fix(auth): handle potential collisions on username --- .../user/auth/POST-user_auth_social.test.js | 35 +++++++++++++++++++ website/server/libs/auth/social.js | 14 ++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/test/api/v3/integration/user/auth/POST-user_auth_social.test.js b/test/api/v3/integration/user/auth/POST-user_auth_social.test.js index 6a29d56b49..f492d8e0a0 100644 --- a/test/api/v3/integration/user/auth/POST-user_auth_social.test.js +++ b/test/api/v3/integration/user/auth/POST-user_auth_social.test.js @@ -65,6 +65,41 @@ describe('POST /user/auth/social', () => { await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user'); }); + it('includes sanitized version of provided username', async () => { + const response = await api.post(endpoint, { + authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase + network, + username: 'Google User Name', + }); + + await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.equal('GoogleUserName'); + await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.equal('googleusername'); + }); + + it('generates a random username if provided username is entirely invalid', async () => { + const response = await api.post(endpoint, { + authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase + network, + username: 'Áîüè', + }); + + await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.contain('hb-'); + await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.contain('hb-'); + }); + + it('generates a random username if sanitized username conflicts with an extant user', async () => { + user = generateUser({ 'auth.local.username': 'GoogleUserName' }); + + const response = await api.post(endpoint, { + authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase + network, + username: 'Google User Name', + }); + + await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.contain('hb-'); + await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.contain('hb-'); + }); + it('fails if allowRegister is false and user does not exist', async () => { await expect(api.post(endpoint, { authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase diff --git a/website/server/libs/auth/social.js b/website/server/libs/auth/social.js index d234ec632e..72b1bee872 100644 --- a/website/server/libs/auth/social.js +++ b/website/server/libs/auth/social.js @@ -70,7 +70,7 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre if (!user.auth.local.email) { user.auth.local.email = await socialEmailToLocal(user); } - // Force the updated timestampt to update, so that we know they logged in + // Force the updated timestamp to save, so that we know they logged in user.auth.timestamps.updated = new Date(); await user.save(); return loginRes(user, req, res); @@ -82,7 +82,10 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre } if (!existingUser && email) { - existingUser = await User.findOne({ 'auth.local.email': email }).exec(); + existingUser = await User.findOne( + { 'auth.local.email': email }, + { auth: 1 }, + ).exec(); } if (!allowRegister && !existingUser) { @@ -101,6 +104,13 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre let sanitizedUsername = username.replace(/[^a-zA-Z0-9_-]/g, ''); if (!sanitizedUsername) { sanitizedUsername = generateUsername(); + } else { + const conflictingUser = await User.findOne({ + 'auth.local.lowerCaseUsername': sanitizedUsername.toLowerCase(), + }, { _id: 1 }); + if (conflictingUser) { + sanitizedUsername = generateUsername(); + } } if (existingUser) { From 5c555cbf888597beff4cb96a51ea95aea899c6ad Mon Sep 17 00:00:00 2001 From: Kalista Payne Date: Wed, 7 Jan 2026 17:52:33 -0600 Subject: [PATCH 7/8] fix(auth): revert attempted perf tweak --- website/server/libs/auth/social.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/website/server/libs/auth/social.js b/website/server/libs/auth/social.js index 72b1bee872..6d3ac59d12 100644 --- a/website/server/libs/auth/social.js +++ b/website/server/libs/auth/social.js @@ -82,10 +82,8 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre } if (!existingUser && email) { - existingUser = await User.findOne( - { 'auth.local.email': email }, - { auth: 1 }, - ).exec(); + // TODO we load the whole user object here. Is that necessary? + existingUser = await User.findOne({ 'auth.local.email': email }).exec(); } if (!allowRegister && !existingUser) { From 215b26acacf7274ec67083ca66a1c098fda5e49f Mon Sep 17 00:00:00 2001 From: Kalista Payne Date: Tue, 13 Jan 2026 17:01:09 -0600 Subject: [PATCH 8/8] fix(auth): run the actual verify check --- .../user/auth/POST-user_auth_social.test.js | 15 +++++++++++++-- website/server/libs/auth/social.js | 4 +++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/test/api/v3/integration/user/auth/POST-user_auth_social.test.js b/test/api/v3/integration/user/auth/POST-user_auth_social.test.js index f492d8e0a0..a447bf68d7 100644 --- a/test/api/v3/integration/user/auth/POST-user_auth_social.test.js +++ b/test/api/v3/integration/user/auth/POST-user_auth_social.test.js @@ -76,7 +76,7 @@ describe('POST /user/auth/social', () => { await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.equal('googleusername'); }); - it('generates a random username if provided username is entirely invalid', async () => { + it('generates a random username if provided username contains only disallowed characters', async () => { const response = await api.post(endpoint, { authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase network, @@ -87,8 +87,19 @@ describe('POST /user/auth/social', () => { await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.contain('hb-'); }); + it('generates a random username if provided username contains a disallowed word', async () => { + const response = await api.post(endpoint, { + authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase + network, + username: 'i am a TESTPLACEHOLDERSLURWORDHERE', + }); + + await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.contain('hb-'); + await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.contain('hb-'); + }); + it('generates a random username if sanitized username conflicts with an extant user', async () => { - user = generateUser({ 'auth.local.username': 'GoogleUserName' }); + user = await generateUser({ 'auth.local.username': 'GoogleUserName' }); const response = await api.post(endpoint, { authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase diff --git a/website/server/libs/auth/social.js b/website/server/libs/auth/social.js index 6d3ac59d12..945d06f81a 100644 --- a/website/server/libs/auth/social.js +++ b/website/server/libs/auth/social.js @@ -1,6 +1,7 @@ import pick from 'lodash/pick'; import passport from 'passport'; import common from '../../../common'; +import { verifyUsername } from '../user/validation'; import { BadRequest, NotAuthorized, NotFound } from '../errors'; import logger from '../logger'; import { @@ -100,7 +101,8 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre } let sanitizedUsername = username.replace(/[^a-zA-Z0-9_-]/g, ''); - if (!sanitizedUsername) { + const issues = verifyUsername(sanitizedUsername, res, true); + if (issues.length > 0) { sanitizedUsername = generateUsername(); } else { const conflictingUser = await User.findOne({