From cb4064ce8420e9f0f80184853a5c40e57a703953 Mon Sep 17 00:00:00 2001 From: Lisa Chan Date: Fri, 13 Mar 2026 11:11:25 -0400 Subject: [PATCH] fix(emails): Fix user's locale in emails from being overwritten by another user --- .../src/renderer/email-helpers.spec.ts | 55 +++++++++++++++++++ .../src/renderer/email-helpers.ts | 20 ++----- packages/fxa-auth-server/lib/senders/email.js | 21 ++----- 3 files changed, 67 insertions(+), 29 deletions(-) diff --git a/libs/accounts/email-renderer/src/renderer/email-helpers.spec.ts b/libs/accounts/email-renderer/src/renderer/email-helpers.spec.ts index c2004a5a681..ef5098bb91d 100644 --- a/libs/accounts/email-renderer/src/renderer/email-helpers.spec.ts +++ b/libs/accounts/email-renderer/src/renderer/email-helpers.spec.ts @@ -136,5 +136,60 @@ describe('EmailHelpers', () => { expect(resultDefault).toEqual('01/15/2024'); expect(resultEst).toEqual('01/14/2024'); }); + + it('does not leak locale between concurrent calls', async () => { + const date = new Date('2025-03-13T12:00:00Z'); + + const [enResult, gbResult] = await Promise.all([ + Promise.resolve(constructLocalDateString(undefined, 'en', date)), + Promise.resolve(constructLocalDateString(undefined, 'en-GB', date)), + ]); + + expect(enResult).toEqual('03/13/2025'); + expect(gbResult).toEqual('13/03/2025'); + }); + + it('does not leak locale into subsequent calls', () => { + const date = new Date('2025-03-13T12:00:00Z'); + + constructLocalDateString(undefined, 'en-GB', date); + const enResult = constructLocalDateString(undefined, 'en', date); + + expect(enResult).toEqual('03/13/2025'); + }); + }); + + describe('constructLocalTimeAndDateStrings - locale isolation', () => { + it('does not leak locale between concurrent calls', async () => { + const [enResult, esResult] = await Promise.all([ + Promise.resolve( + constructLocalTimeAndDateStrings('America/Los_Angeles', 'en') + ), + Promise.resolve( + constructLocalTimeAndDateStrings('America/Los_Angeles', 'es') + ), + ]); + + const enDays = [ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday', + ]; + const esDays = [ + 'lunes', + 'martes', + 'miércoles', + 'jueves', + 'viernes', + 'sábado', + 'domingo', + ]; + expect(enDays).toContain(enResult.date.split(',')[0]); + expect(esDays).toContain(esResult.date.split(',')[0]); + }); }); }); diff --git a/libs/accounts/email-renderer/src/renderer/email-helpers.ts b/libs/accounts/email-renderer/src/renderer/email-helpers.ts index ef9f9d917b8..f2152ffc4a3 100644 --- a/libs/accounts/email-renderer/src/renderer/email-helpers.ts +++ b/libs/accounts/email-renderer/src/renderer/email-helpers.ts @@ -53,15 +53,11 @@ export const constructLocalTimeAndDateStrings = ( time: string; timeZone: string; } => { - moment.tz.setDefault(DEFAULT_TIMEZONE); - const locale = determineLocale(acceptLanguage) || DEFAULT_LOCALE; - moment.locale(locale); - let timeMoment = moment(date ? date : undefined); - if (timeZone) { - timeMoment = timeMoment.tz(timeZone); - } + const timeMoment = moment(date ? date : undefined) + .locale(locale) + .tz(timeZone || DEFAULT_TIMEZONE); const formattedTime = timeMoment.format('LTS (z)'); const formattedDate = timeMoment.format('dddd, ll'); @@ -88,15 +84,11 @@ export const constructLocalDateString = ( date?: Date | number, formatString = 'L' ): string => { - moment.tz.setDefault(DEFAULT_TIMEZONE); - const locale = determineLocale(acceptLanguage) || DEFAULT_LOCALE; - moment.locale(locale); - let time = moment(date); - if (timeZone) { - time = time.tz(timeZone); - } + const time = moment(date) + .locale(locale) + .tz(timeZone || DEFAULT_TIMEZONE); return time.format(formatString); }; diff --git a/packages/fxa-auth-server/lib/senders/email.js b/packages/fxa-auth-server/lib/senders/email.js index f93570744ab..bd0ed89a1e4 100644 --- a/packages/fxa-auth-server/lib/senders/email.js +++ b/packages/fxa-auth-server/lib/senders/email.js @@ -23,7 +23,6 @@ const { ProductConfigurationManager } = require('@fxa/shared/cms'); const { Container } = require('typedi'); const DEFAULT_LOCALE = 'en'; -const DEFAULT_TIMEZONE = 'Etc/UTC'; const UTM_PREFIX = 'fx-'; const X_SES_CONFIGURATION_SET = 'X-SES-CONFIGURATION-SET'; @@ -186,15 +185,11 @@ module.exports = function (log, config, bounces, statsd) { } function constructLocalTimeString(timeZone, locale) { - // if no timeZone is passed, use DEFAULT_TIMEZONE - moment.tz.setDefault(DEFAULT_TIMEZONE); // if no locale is passed, use DEFAULT_LOCALE locale = locale || DEFAULT_LOCALE; - moment.locale(locale); - let timeMoment = moment(); - if (timeZone) { - timeMoment = timeMoment.tz(timeZone); - } + const timeMoment = moment() + .locale(locale) + .tz(timeZone || 'Etc/UTC'); // return a locale-specific time // if date or time is passed, return it as the current date or time const timeNow = timeMoment.format('LTS (z)'); @@ -208,15 +203,11 @@ module.exports = function (log, config, bounces, statsd) { date, formatString = 'L' ) { - // if no timeZone is passed, use DEFAULT_TIMEZONE - moment.tz.setDefault(DEFAULT_TIMEZONE); // if no locale is passed, use DEFAULT_LOCALE locale = locale || DEFAULT_LOCALE; - moment.locale(locale); - let time = moment(date); - if (timeZone) { - time = time.tz(timeZone); - } + const time = moment(date) + .locale(locale) + .tz(timeZone || 'Etc/UTC'); // return a locale-specific date return time.format(formatString); }