diff --git a/CHANGES b/CHANGES index dc3298edc..2b59a2b10 100644 --- a/CHANGES +++ b/CHANGES @@ -2563,4 +2563,5 @@ * Show swaps in disk graphs * disk usage: run once a day * mail: fix 100% cpu use with unreachable servers +* security: do not password reset mail to cloudron owned mail domain diff --git a/src/users.js b/src/users.js index 186cb5de1..e50da6737 100644 --- a/src/users.js +++ b/src/users.js @@ -80,6 +80,7 @@ const appPasswords = require('./apppasswords.js'), eventlog = require('./eventlog.js'), externalLdap = require('./externalldap.js'), hat = require('./hat.js'), + mail = require('./mail.js'), mailer = require('./mailer.js'), mysql = require('mysql'), qrcode = require('qrcode'), @@ -630,24 +631,6 @@ async function getSuperadmins() { return await getByRole(exports.ROLE_OWNER); } -async function sendPasswordResetByIdentifier(identifier, auditSource) { - assert.strictEqual(typeof identifier, 'string'); - assert.strictEqual(typeof auditSource, 'object'); - - const user = identifier.indexOf('@') === -1 ? await getByUsername(identifier.toLowerCase()) : await getByEmail(identifier.toLowerCase()); - if (!user) throw new BoxError(BoxError.NOT_FOUND, 'User not found'); - - const resetToken = hat(256); - const resetTokenCreationTime = new Date(); - - user.resetToken = resetToken; - user.resetTokenCreationTime = resetTokenCreationTime; - await update(user, { resetToken,resetTokenCreationTime }, auditSource); - - const resetLink = `${settings.dashboardOrigin()}/login.html?resetToken=${user.resetToken}`; - await mailer.passwordReset(user, user.fallbackEmail || user.email, resetLink); -} - async function getPasswordResetLink(user, auditSource) { assert.strictEqual(typeof user, 'object'); assert.strictEqual(typeof auditSource, 'object'); @@ -667,6 +650,23 @@ async function getPasswordResetLink(user, auditSource) { return resetLink; } +async function sendPasswordResetByIdentifier(identifier, auditSource) { + assert.strictEqual(typeof identifier, 'string'); + assert.strictEqual(typeof auditSource, 'object'); + + const user = identifier.indexOf('@') === -1 ? await getByUsername(identifier.toLowerCase()) : await getByEmail(identifier.toLowerCase()); + if (!user) throw new BoxError(BoxError.NOT_FOUND, 'User not found'); + + const email = user.fallbackEmail || user.email; + + // security measure to prevent a mail manager or admin resetting the superadmin's password + const mailDomains = await mail.listDomains(); + if (mailDomains.some(d => d.enabled && email.endsWith(`@${d.domain}`))) throw new BoxError(BoxError.CONFLICT, 'Password reset email cannot be sent to email addresses hosted on Cloudron'); + + const resetLink = await getPasswordResetLink(user, auditSource); + await mailer.passwordReset(user, email, resetLink); +} + async function sendPasswordResetEmail(user, email, auditSource) { assert.strictEqual(typeof user, 'object'); assert.strictEqual(typeof email, 'string'); @@ -675,6 +675,10 @@ async function sendPasswordResetEmail(user, email, auditSource) { const error = validateEmail(email); if (error) throw error; + // security measure to prevent a mail manager or admin resetting the superadmin's password + const mailDomains = await mail.listDomains(); + if (mailDomains.some(d => d.enabled && email.endsWith(`@${d.domain}`))) throw new BoxError(BoxError.CONFLICT, 'Password reset email cannot be sent to email addresses hosted on Cloudron'); + const resetLink = await getPasswordResetLink(user, auditSource); await mailer.passwordReset(user, email, resetLink); }