diff --git a/app/config/console.php b/app/config/console.php index 70bb3523e6..f8f68a8039 100644 --- a/app/config/console.php +++ b/app/config/console.php @@ -49,7 +49,7 @@ $console = [ 'githubSecret' => System::getEnv('_APP_CONSOLE_GITHUB_SECRET', ''), 'githubAppid' => System::getEnv('_APP_CONSOLE_GITHUB_APP_ID', '') ], - 'smtpBaseTemplate' => 'email-base-styled', + 'smtpBaseTemplate' => APP_BRANDED_EMAIL_BASE_TEMPLATE, ]; return $console; diff --git a/app/config/locale/templates/email-base-styled.tpl b/app/config/locale/templates/email-base-styled.tpl index 3f3ba8dd9c..37ca630d43 100644 --- a/app/config/locale/templates/email-base-styled.tpl +++ b/app/config/locale/templates/email-base-styled.tpl @@ -131,6 +131,14 @@ .social-icon > img { margin: auto; } + p.security-phrase:not(:empty) { + opacity: 0.7; + margin: 0; + padding: 0; + margin-top: 32px; + padding-top: 32px; + border-top: 1px solid #e8e9f0; + } diff --git a/app/config/locale/templates/email-base.tpl b/app/config/locale/templates/email-base.tpl index 5153b3e4fc..de632d7838 100644 --- a/app/config/locale/templates/email-base.tpl +++ b/app/config/locale/templates/email-base.tpl @@ -109,10 +109,15 @@ h* { font-family: 'Poppins', sans-serif; } - p { margin-bottom: 10px; } + p.security-phrase:not(:empty) { + opacity: 0.7; + margin-top: 32px; + padding-top: 32px; + border-top: 1px solid #e8e9f0; + } diff --git a/app/config/locale/templates/email-otp.tpl b/app/config/locale/templates/email-otp.tpl index e18a4ce725..cfcdb8f7af 100644 --- a/app/config/locale/templates/email-otp.tpl +++ b/app/config/locale/templates/email-otp.tpl @@ -15,6 +15,4 @@
{{thanks}}
{{signature}}
-{{securityPhrase}}
\ No newline at end of file +{{securityPhrase}}
\ No newline at end of file diff --git a/app/config/locale/translations/en.json b/app/config/locale/translations/en.json index 69328e61a7..0500c4c668 100644 --- a/app/config/locale/translations/en.json +++ b/app/config/locale/translations/en.json @@ -5,7 +5,7 @@ "emails.sender": "{{project}} Team", "emails.verification.subject": "Account Verification", "emails.verification.preview": "Verify your email to activate your {{project}} account.", - "emails.verification.heading": "Verify your email to start using Appwrite Cloud", + "emails.verification.heading": "Verify your email to start using {{project}}", "emails.verification.hello": "Hello {{user}},", "emails.verification.body": "Follow this link to verify your email address to your {{b}}{{project}}{{/b}} account.", "emails.verification.footer": "If you didn’t ask to verify this address, you can ignore this message.", @@ -34,7 +34,7 @@ "emails.sessionAlert.signature": "{{project}} team", "emails.otpSession.subject": "OTP for {{project}} Login", "emails.otpSession.preview": "Use OTP {{otp}} to sign in to {{project}}. Expires in 15 minutes.", - "emails.otpSession.heading": "Login with OTP to use Appwrite Cloud", + "emails.otpSession.heading": "Login with OTP to use {{project}}", "emails.otpSession.hello": "Hello {{user}},", "emails.otpSession.description": "Enter the following verification code when prompted to securely sign in to your {{b}}{{project}}{{/b}} account. This code will expire in 15 minutes.", "emails.otpSession.clientInfo": "This sign in was requested using {{b}}{{agentClient}}{{/b}} on {{b}}{{agentDevice}}{{/b}} {{b}}{{agentOs}}{{/b}}. If you didn't request the sign in, you can safely ignore this email.", @@ -43,7 +43,7 @@ "emails.otpSession.signature": "{{project}} team", "emails.mfaChallenge.subject": "Verification Code for {{project}}", "emails.mfaChallenge.preview": "Use code {{otp}} for two-step verification in {{project}}. Expires in 15 minutes.", - "emails.mfaChallenge.heading": "Complete two-step verification to use Appwrite Cloud", + "emails.mfaChallenge.heading": "Complete two-step verification to use {{project}}", "emails.mfaChallenge.hello": "Hello {{user}},", "emails.mfaChallenge.description": "Enter the following code to confirm your two-step verification in {{b}}{{project}}{{/b}}. This code will expire in 15 minutes.", "emails.mfaChallenge.clientInfo": "This verification code was requested using {{b}}{{agentClient}}{{/b}} on {{b}}{{agentDevice}}{{/b}} {{b}}{{agentOs}}{{/b}}. If you didn't request the verification code, you can safely ignore this email.", diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index e98376dfc7..af9a772b17 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -69,6 +69,12 @@ use Utopia\Validator\WhiteList; $oauthDefaultSuccess = '/console/auth/oauth2/success'; $oauthDefaultFailure = '/console/auth/oauth2/failure'; +function containsDirectoryTraversal(string $path) +{ + // Matches '../', './', '/..', or absolute paths starting with '/' + return preg_match('/(\.\.\/|\.\/|\/\.\.|\/)/', $path); +} + function sendSessionAlert(Locale $locale, Document $user, Document $project, Document $session, Mail $queueForMails) { $subject = $locale->getText("emails.sessionAlert.subject"); @@ -2300,6 +2306,11 @@ App::post('/v1/account/tokens/email') $customTemplate = $project->getAttribute('templates', [])['email.otpSession-' . $locale->default] ?? []; $smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base'); + + if (containsDirectoryTraversal($smtpBaseTemplate)) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid template path'); + } + $bodyTemplate = __DIR__ . '/../../config/locale/templates/' . $smtpBaseTemplate . '.tpl'; $detector = new Detector($request->getUserAgent('UNKNOWN')); @@ -2317,10 +2328,8 @@ App::post('/v1/account/tokens/email') if (!empty($phrase)) { $message->setParam('{{securityPhrase}}', $locale->getText("emails.otpSession.securityPhrase")); - $message->setParam('{{securityPhraseDividerDisplay}}', 'block'); } else { $message->setParam('{{securityPhrase}}', ''); - $message->setParam('{{securityPhraseDividerDisplay}}', 'none'); } $body = $message->render(); @@ -2372,6 +2381,7 @@ App::post('/v1/account/tokens/email') } $emailVariables = [ + 'heading' => $heading, 'direction' => $locale->getText('settings.direction'), // {{user}}, {{project}} and {{otp}} are required in the templates 'user' => $user->getAttribute('name'), @@ -2385,9 +2395,8 @@ App::post('/v1/account/tokens/email') 'team' => '', ]; - if ($smtpBaseTemplate === 'email-base-styled') { + if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) { $emailVariables = array_merge($emailVariables, [ - 'heading' => $heading, 'accentColor' => APP_EMAIL_ACCENT_COLOR, 'logoUrl' => APP_EMAIL_LOGO_URL, 'twitterUrl' => APP_SOCIAL_TWITTER, @@ -3611,6 +3620,11 @@ App::post('/v1/account/verification') $customTemplate = $project->getAttribute('templates', [])['email.verification-' . $locale->default] ?? []; $smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base'); + + if (containsDirectoryTraversal($smtpBaseTemplate)) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid template path'); + } + $bodyTemplate = __DIR__ . '/../../config/locale/templates/' . $smtpBaseTemplate . '.tpl'; $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl'); @@ -3671,6 +3685,7 @@ App::post('/v1/account/verification') } $emailVariables = [ + 'heading' => $heading, 'direction' => $locale->getText('settings.direction'), // {{user}}, {{redirect}} and {{project}} are required in default and custom templates 'user' => $user->getAttribute('name'), @@ -3680,9 +3695,8 @@ App::post('/v1/account/verification') 'team' => '', ]; - if ($smtpBaseTemplate === 'email-base-styled') { + if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) { $emailVariables = array_merge($emailVariables, [ - 'heading' => $heading, 'accentColor' => APP_EMAIL_ACCENT_COLOR, 'logoUrl' => APP_EMAIL_LOGO_URL, 'twitterUrl' => APP_SOCIAL_TWITTER, @@ -4707,6 +4721,11 @@ App::post('/v1/account/mfa/challenge') $customTemplate = $project->getAttribute('templates', [])['email.mfaChallenge-' . $locale->default] ?? []; $smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base'); + + if (containsDirectoryTraversal($smtpBaseTemplate)) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid template path'); + } + $bodyTemplate = __DIR__ . '/../../config/locale/templates/' . $smtpBaseTemplate . '.tpl'; $detector = new Detector($request->getUserAgent('UNKNOWN')); @@ -4771,6 +4790,7 @@ App::post('/v1/account/mfa/challenge') } $emailVariables = [ + 'heading' => $heading, 'direction' => $locale->getText('settings.direction'), // {{user}}, {{project}} and {{otp}} are required in the templates 'user' => $user->getAttribute('name'), @@ -4781,9 +4801,8 @@ App::post('/v1/account/mfa/challenge') 'agentOs' => $agentOs['osName'] ?? 'UNKNOWN', ]; - if ($smtpBaseTemplate === 'email-base-styled') { + if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) { $emailVariables = array_merge($emailVariables, [ - 'heading' => $heading, 'accentColor' => APP_EMAIL_ACCENT_COLOR, 'logoUrl' => APP_EMAIL_LOGO_URL, 'twitterUrl' => APP_SOCIAL_TWITTER, diff --git a/app/init/constants.php b/app/init/constants.php index 28cf8a4052..16ddcf5551 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -85,6 +85,7 @@ const APP_PLATFORM_CLIENT = 'client'; const APP_PLATFORM_CONSOLE = 'console'; const APP_VCS_GITHUB_USERNAME = 'Appwrite'; const APP_VCS_GITHUB_EMAIL = 'team@appwrite.io'; +const APP_BRANDED_EMAIL_BASE_TEMPLATE = 'email-base-styled'; // Database Reconnect const DATABASE_RECONNECT_SLEEP = 2;