From 1cc2131fed96a14fc2017d430d2ddec2758798e0 Mon Sep 17 00:00:00 2001 From: Jamie Getty Date: Wed, 3 Dec 2025 12:34:52 +1300 Subject: [PATCH 1/8] new: Only generate password reset code prior to email being sent --- .../controllers/aaf/vhr/LostPasswordController.groovy | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/virtualhome/grails-app/controllers/aaf/vhr/LostPasswordController.groovy b/virtualhome/grails-app/controllers/aaf/vhr/LostPasswordController.groovy index d934d3c4..e299959c 100644 --- a/virtualhome/grails-app/controllers/aaf/vhr/LostPasswordController.groovy +++ b/virtualhome/grails-app/controllers/aaf/vhr/LostPasswordController.groovy @@ -42,6 +42,11 @@ class LostPasswordController { def managedSubjectInstance = ManagedSubject.findWhere(login: params.login) if (managedSubjectInstance) { + + def smsCode = aaf.vhr.crypto.CryptoUtil.randomAlphanumeric(grailsApplication.config.aaf.vhr.passwordreset.reset_code_length) + managedSubjectInstance.resetCodeExternal = smsCode + managedSubjectInstance.save() + lostPasswordService.sendResetEmail(managedSubjectInstance) } @@ -69,9 +74,6 @@ class LostPasswordController { session.setAttribute(CURRENT_USER, managedSubjectInstance.id) - // If we haven't generated an SMS code already, generate an SMS code and sent it to the user (even if we have already sent one) - def smsCode = aaf.vhr.crypto.CryptoUtil.randomAlphanumeric(grailsApplication.config.aaf.vhr.passwordreset.reset_code_length) - managedSubjectInstance.resetCodeExternal = smsCode flash.type = 'info' flash.message = 'controllers.aaf.vhr.lostpassword.reset.sent.externalcode' sendResetCodes(managedSubjectInstance) From 1d122ae27c69898fb04dbd688d8f7175c14454f3 Mon Sep 17 00:00:00 2001 From: Jamie Getty Date: Wed, 3 Dec 2025 15:13:16 +1300 Subject: [PATCH 2/8] new: only send sms messages for valid phone numbers --- .../aaf/vhr/LostPasswordController.groovy | 28 +++++++++++++------ .../grails-app/i18n/messages.properties | 1 + 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/virtualhome/grails-app/controllers/aaf/vhr/LostPasswordController.groovy b/virtualhome/grails-app/controllers/aaf/vhr/LostPasswordController.groovy index e299959c..2f9675b8 100644 --- a/virtualhome/grails-app/controllers/aaf/vhr/LostPasswordController.groovy +++ b/virtualhome/grails-app/controllers/aaf/vhr/LostPasswordController.groovy @@ -74,10 +74,15 @@ class LostPasswordController { session.setAttribute(CURRENT_USER, managedSubjectInstance.id) + if (!sendResetCodes(managedSubjectInstance)) { + flash.type = 'error' + flash.message = 'controllers.aaf.vhr.lostpassword.mobile.invalid' + redirect action: 'unavailable' + return + } + flash.type = 'info' flash.message = 'controllers.aaf.vhr.lostpassword.reset.sent.externalcode' - sendResetCodes(managedSubjectInstance) - redirect action: 'reset' } @@ -217,14 +222,19 @@ Remote IP: ${request.getRemoteAddr()}""" true } - private void sendResetCodes(ManagedSubject managedSubjectInstance) { - // SMS reset code (UI asks to contact admin if no mobile) - if(managedSubjectInstance.mobileNumber) { - if(!sendsms(managedSubjectInstance)) { - redirect action: 'unavailable' - return - } + private boolean sendResetCodes(ManagedSubject managedSubjectInstance) { + + if(!managedSubjectInstance.mobileNumber) { + log.error "Cannot send SMS to ${managedSubjectInstance} since they have no mobile number!" + return false; } + + if(!ManagedSubject.validMobileNumber(managedSubjectInstance.mobileNumber, managedSubjectInstance)) { + log.error "Cannot send SMS to ${managedSubjectInstance} since they have an invalid mobile number ${managedSubjectInstance.mobileNumber} !" + return false + } + + sendsms(managedSubjectInstance) } private boolean sendsms(ManagedSubject managedSubjectInstance) { diff --git a/virtualhome/grails-app/i18n/messages.properties b/virtualhome/grails-app/i18n/messages.properties index c7a4a2b0..4dca08a2 100644 --- a/virtualhome/grails-app/i18n/messages.properties +++ b/virtualhome/grails-app/i18n/messages.properties @@ -400,6 +400,7 @@ controllers.aaf.vhr.lostpassword.reset.sent.externalcode=Your password reset cod controllers.aaf.vhr.lostpassword.reset.sent.email=Your password reset code has been sent via an email. Please allow at least 5 minutes for codes to be delivered. controllers.aaf.vhr.lostpassword.reset.mobile.missing=You must have a mobile number configured to receive an SMS code. Please contact your administrators for more information. controllers.aaf.vhr.lostpassword.reset.url.badsecret=An error has occurred while attempting to reset your password. Please try again. +controllers.aaf.vhr.lostpassword.mobile.invalid=You cannot be sent a reset code because of an issue with your configured mobile number. Please contact an administrator to fix this. controllers.aaf.vhr.lostusername.email.subject=Your Tuakiri Virtual Home username controllers.aaf.vhr.lostusername.recaptcha.error=Data supplied for the recaptcha field could not be verified From 96fad3d117f190ceae12462ff3dd9b54233b5374 Mon Sep 17 00:00:00 2001 From: Jamie Getty Date: Wed, 3 Dec 2025 15:41:34 +1300 Subject: [PATCH 3/8] new: Remove hyphens from phone numbers during validation --- virtualhome/grails-app/domain/aaf/vhr/ManagedSubject.groovy | 2 ++ 1 file changed, 2 insertions(+) diff --git a/virtualhome/grails-app/domain/aaf/vhr/ManagedSubject.groovy b/virtualhome/grails-app/domain/aaf/vhr/ManagedSubject.groovy index 68451b97..24d41442 100644 --- a/virtualhome/grails-app/domain/aaf/vhr/ManagedSubject.groovy +++ b/virtualhome/grails-app/domain/aaf/vhr/ManagedSubject.groovy @@ -517,6 +517,8 @@ class ManagedSubject { checkedNumber = checkedNumber.replace(' ','') + checkedNumber = checkedNumber.replace('-','') + if(!checkedNumber.startsWith('+')) { return false } else { From cf51c0bb6bcd00974062e8af54e13c6a7749429c Mon Sep 17 00:00:00 2001 From: Jamie Getty Date: Wed, 3 Dec 2025 16:14:40 +1300 Subject: [PATCH 4/8] new: Add error handling to all sendResetCodes calls --- .../controllers/aaf/vhr/LostPasswordController.groovy | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/virtualhome/grails-app/controllers/aaf/vhr/LostPasswordController.groovy b/virtualhome/grails-app/controllers/aaf/vhr/LostPasswordController.groovy index 2f9675b8..4f086558 100644 --- a/virtualhome/grails-app/controllers/aaf/vhr/LostPasswordController.groovy +++ b/virtualhome/grails-app/controllers/aaf/vhr/LostPasswordController.groovy @@ -105,7 +105,12 @@ class LostPasswordController { flash.type = 'error' flash.message = 'controllers.aaf.vhr.lostpassword.resend.error' } else { - sendResetCodes(managedSubjectInstance) + if (!sendResetCodes(managedSubjectInstance)) { + flash.type = 'error' + flash.message = 'controllers.aaf.vhr.lostpassword.mobile.invalid' + redirect action: 'unavailable' + return + } managedSubjectInstance.lastCodeResend = new Date() managedSubjectInstance.save() From d6e66f49263e2afa4908bf2da0d3c5bb618b3b32 Mon Sep 17 00:00:00 2001 From: Jamie Getty Date: Wed, 3 Dec 2025 16:46:26 +1300 Subject: [PATCH 5/8] new: Ignore HEAD requests to obtainsubject to prevent multiple SMS messages --- .../controllers/aaf/vhr/LostPasswordController.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/virtualhome/grails-app/controllers/aaf/vhr/LostPasswordController.groovy b/virtualhome/grails-app/controllers/aaf/vhr/LostPasswordController.groovy index 4f086558..4293142c 100644 --- a/virtualhome/grails-app/controllers/aaf/vhr/LostPasswordController.groovy +++ b/virtualhome/grails-app/controllers/aaf/vhr/LostPasswordController.groovy @@ -9,7 +9,7 @@ import aaf.vhr.switchch.vho.DeprecatedSubject class LostPasswordController { - static allowedMethods = [emailed: 'POST', validatereset: 'POST'] + static allowedMethods = [emailed: 'POST', validatereset: 'POST', obtainsubject: 'GET'] final String CURRENT_USER = "aaf.vhr.LostPasswordController.CURRENT_USER" final String EMAIL_CODE_SUBJECT ='controllers.aaf.vhr.lostpassword.email.code.subject' From 43f6410df3c137790a5a6631071dff5dbd391cf0 Mon Sep 17 00:00:00 2001 From: Jamie Getty Date: Mon, 8 Dec 2025 17:17:06 +1300 Subject: [PATCH 6/8] fix: Update invalid mobile message --- virtualhome/grails-app/i18n/messages.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/virtualhome/grails-app/i18n/messages.properties b/virtualhome/grails-app/i18n/messages.properties index 4dca08a2..3b789f17 100644 --- a/virtualhome/grails-app/i18n/messages.properties +++ b/virtualhome/grails-app/i18n/messages.properties @@ -400,7 +400,7 @@ controllers.aaf.vhr.lostpassword.reset.sent.externalcode=Your password reset cod controllers.aaf.vhr.lostpassword.reset.sent.email=Your password reset code has been sent via an email. Please allow at least 5 minutes for codes to be delivered. controllers.aaf.vhr.lostpassword.reset.mobile.missing=You must have a mobile number configured to receive an SMS code. Please contact your administrators for more information. controllers.aaf.vhr.lostpassword.reset.url.badsecret=An error has occurred while attempting to reset your password. Please try again. -controllers.aaf.vhr.lostpassword.mobile.invalid=You cannot be sent a reset code because of an issue with your configured mobile number. Please contact an administrator to fix this. +controllers.aaf.vhr.lostpassword.mobile.invalid=A reset code could not be sent to you because of an issue with your configured mobile number. Please contact one of our administrators to address this. controllers.aaf.vhr.lostusername.email.subject=Your Tuakiri Virtual Home username controllers.aaf.vhr.lostusername.recaptcha.error=Data supplied for the recaptcha field could not be verified From 335d62eaf12f521b546dc1d5d25fb2031f22aa2f Mon Sep 17 00:00:00 2001 From: Jamie Getty Date: Tue, 9 Dec 2025 16:43:21 +1300 Subject: [PATCH 7/8] new: Allow characters that normally appear in pasted phone numbers but reject others --- .../domain/aaf/vhr/ManagedSubject.groovy | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/virtualhome/grails-app/domain/aaf/vhr/ManagedSubject.groovy b/virtualhome/grails-app/domain/aaf/vhr/ManagedSubject.groovy index 24d41442..a643a4aa 100644 --- a/virtualhome/grails-app/domain/aaf/vhr/ManagedSubject.groovy +++ b/virtualhome/grails-app/domain/aaf/vhr/ManagedSubject.groovy @@ -515,15 +515,20 @@ class ManagedSubject { checkedNumber = "+64$checkedNumber" } - checkedNumber = checkedNumber.replace(' ','') + // Silently remove anything that might be entered by users in a valid-looking phone number. + checkedNumber = checkedNumber.replaceAll("[ .\\-()]", '') - checkedNumber = checkedNumber.replace('-','') + // Valid phone numbers for the app only contain '+' and 0-9. Anything else is probably junk that users will be warned about. + def newNumber = checkedNumber.replaceAll("[^0-9+]", '') + if (newNumber != checkedNumber) { + return false + } - if(!checkedNumber.startsWith('+')) { + if(!newNumber.startsWith('+')) { return false - } else { - obj.mobileNumber = checkedNumber - return true } + + obj.mobileNumber = newNumber + return true } } From 2f2a889248ada31d43e7696dc68c66ff3a5e71c5 Mon Sep 17 00:00:00 2001 From: Jamie Getty Date: Tue, 9 Dec 2025 17:16:49 +1300 Subject: [PATCH 8/8] new: Make regex more clear --- virtualhome/grails-app/domain/aaf/vhr/ManagedSubject.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/virtualhome/grails-app/domain/aaf/vhr/ManagedSubject.groovy b/virtualhome/grails-app/domain/aaf/vhr/ManagedSubject.groovy index a643a4aa..c0496498 100644 --- a/virtualhome/grails-app/domain/aaf/vhr/ManagedSubject.groovy +++ b/virtualhome/grails-app/domain/aaf/vhr/ManagedSubject.groovy @@ -516,7 +516,7 @@ class ManagedSubject { } // Silently remove anything that might be entered by users in a valid-looking phone number. - checkedNumber = checkedNumber.replaceAll("[ .\\-()]", '') + checkedNumber = checkedNumber.replaceAll("[- .()]", '') // Valid phone numbers for the app only contain '+' and 0-9. Anything else is probably junk that users will be warned about. def newNumber = checkedNumber.replaceAll("[^0-9+]", '')