From 38da602ee1cfc35c0d7918c298fbfc3f3674133b Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Wed, 31 Jul 2024 14:21:10 +0200 Subject: [PATCH] fix: sanitize output for email (#8373) # Which Problems Are Solved ZITADEL uses HTML for emails and renders certain information such as usernames dynamically. That information can be entered by users or administrators. Due to a missing output sanitization, these emails could include malicious code. This may potentially lead to a threat where an attacker, without privileges, could send out altered notifications that are part of the registration processes. An attacker could create a malicious link, where the injected code would be rendered as part of the email. During investigation of this issue a related issue was found and mitigated, where on the user's detail page the username was not sanitized and would also render HTML, giving an attacker the same vulnerability. While it was possible to inject HTML including javascript, the execution of such scripts would be prevented by most email clients and the Content Security Policy in Console UI. # How the Problems Are Solved - All arguments used for email are sanitized (`html.EscapeString`) - The email text no longer `html.UnescapeString` (HTML in custom text is still possible) - Console no longer uses `[innerHtml]` to render the username # Additional Changes None # Additional Context - raised via email --------- Co-authored-by: peintnermax (cherry picked from commit 189505c80fa639108488f5979fe52967df9729fa) --- .../modules/top-view/top-view.component.html | 2 +- .../modules/top-view/top-view.component.scss | 2 +- .../granted-project-detail.component.html | 4 +--- console/src/assets/i18n/bg.json | 2 +- console/src/assets/i18n/cs.json | 2 +- console/src/assets/i18n/de.json | 2 +- console/src/assets/i18n/en.json | 2 +- console/src/assets/i18n/es.json | 2 +- console/src/assets/i18n/fr.json | 2 +- console/src/assets/i18n/it.json | 2 +- console/src/assets/i18n/ja.json | 2 +- console/src/assets/i18n/mk.json | 2 +- console/src/assets/i18n/nl.json | 2 +- console/src/assets/i18n/pl.json | 2 +- console/src/assets/i18n/pt.json | 2 +- console/src/assets/i18n/ru.json | 3 +-- console/src/assets/i18n/zh.json | 2 +- .../notification/templates/templateData.go | 3 +-- internal/notification/types/notification.go | 20 +++++++++++++++++++ 19 files changed, 38 insertions(+), 22 deletions(-) diff --git a/console/src/app/modules/top-view/top-view.component.html b/console/src/app/modules/top-view/top-view.component.html index 65e68dec7a..7ef4c27e1d 100644 --- a/console/src/app/modules/top-view/top-view.component.html +++ b/console/src/app/modules/top-view/top-view.component.html @@ -16,7 +16,7 @@ >
- + {{ sub }} info_outline diff --git a/console/src/app/modules/top-view/top-view.component.scss b/console/src/app/modules/top-view/top-view.component.scss index a86282326f..4815cefef4 100644 --- a/console/src/app/modules/top-view/top-view.component.scss +++ b/console/src/app/modules/top-view/top-view.component.scss @@ -82,7 +82,7 @@ .cnsl-type { font-size: 14px; - margin-top: 2rem; + margin-top: 1.5rem; margin-bottom: 1rem; } diff --git a/console/src/app/pages/projects/granted-projects/granted-project-detail/granted-project-detail.component.html b/console/src/app/pages/projects/granted-projects/granted-project-detail/granted-project-detail.component.html index c04e428d05..17a41cce91 100644 --- a/console/src/app/pages/projects/granted-projects/granted-project-detail/granted-project-detail.component.html +++ b/console/src/app/pages/projects/granted-projects/granted-project-detail/granted-project-detail.component.html @@ -2,9 +2,7 @@ title="{{ project?.projectName }}" [hasActions]="false" docLink="https://zitadel.com/docs/guides/manage/console/projects#what-is-a-granted-project" - sub="{{ 'PROJECT.PAGES.TYPE.GRANTED_SINGULAR' | translate }} {{ 'ACTIONS.OF' | translate }} {{ - project?.projectOwnerName - }}" + sub="{{ 'PROJECT.PAGES.TYPE.GRANTED_SINGULAR' | translate: { name: project?.projectOwnerName } }}" [isActive]="project?.state === ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE" [isInactive]="project?.state === ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE" stateTooltip="{{ 'ORG.STATE.' + project?.state | translate }}" diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json index 4a1131920a..8676366ce9 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -1770,7 +1770,7 @@ "TYPE": { "OWNED": "Притежавани проекти", "OWNED_SINGULAR": "Собствен проект", - "GRANTED_SINGULAR": "Приет проект" + "GRANTED_SINGULAR": "Отпуснат проект на {{name}}" }, "PRIVATELABEL": { "0": { diff --git a/console/src/assets/i18n/cs.json b/console/src/assets/i18n/cs.json index f7b098c098..ecc2e6cb41 100644 --- a/console/src/assets/i18n/cs.json +++ b/console/src/assets/i18n/cs.json @@ -1778,7 +1778,7 @@ "TYPE": { "OWNED": "Vlastní projekty", "OWNED_SINGULAR": "Vlastní projekt", - "GRANTED_SINGULAR": "Přidělený projekt" + "GRANTED_SINGULAR": "Projekt přidělený {{name}}" }, "PRIVATELABEL": { "TITLE": "Nastavení brandingu", diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 4c595773c4..7be455590a 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -1776,7 +1776,7 @@ "TYPE": { "OWNED": "Eigene Projekte", "OWNED_SINGULAR": "Eigenes Projekt", - "GRANTED_SINGULAR": "Berechtigtes Projekt" + "GRANTED_SINGULAR": "Berechtigtes Projekt von {{name}}" }, "PRIVATELABEL": { "TITLE": "Branding Verhalten", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 8a50cae2cf..46d24b5cd2 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -1777,7 +1777,7 @@ "TYPE": { "OWNED": "Owned Projects", "OWNED_SINGULAR": "Owned Project", - "GRANTED_SINGULAR": "Granted Project" + "GRANTED_SINGULAR": "Granted Project of {{name}}" }, "PRIVATELABEL": { "TITLE": "Branding Setting", diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json index d535f44252..1687c87e0f 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -1778,7 +1778,7 @@ "TYPE": { "OWNED": "Proyectos propios", "OWNED_SINGULAR": "Proyecto propio", - "GRANTED_SINGULAR": "Proyecto concedido" + "GRANTED_SINGULAR": "Proyecto asignado {{name}}" }, "PRIVATELABEL": { "TITLE": "Ajustes de imagen de marca", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index 110f0589d3..6245d6af01 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -1776,7 +1776,7 @@ "TYPE": { "OWNED": "Projets possédés", "OWNED_SINGULAR": "Projet possédé", - "GRANTED_SINGULAR": "Projet octroyé" + "GRANTED_SINGULAR": "Projet attribué à {{name}}" }, "PRIVATELABEL": { "TITLE": "Image de marque", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index 258635148e..768ad2b66c 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -1776,7 +1776,7 @@ "TYPE": { "OWNED": "Progetti proprietari", "OWNED_SINGULAR": "Progetto proprietario", - "GRANTED_SINGULAR": "Progetto delegato" + "GRANTED_SINGULAR": "Progetto concesso di {{name}}" }, "PRIVATELABEL": { "TITLE": "Impostazione branding", diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index 30f1f8485c..857b0d71ad 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -1773,7 +1773,7 @@ "TYPE": { "OWNED": "所有プロジェクト", "OWNED_SINGULAR": "所有プロジェクト", - "GRANTED_SINGULAR": "グラントされたプロジェクト" + "GRANTED_SINGULAR": "{{name}}に付与されたプロジェクト" }, "PRIVATELABEL": { "TITLE": "ブランディング設定", diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json index 55f52bc24b..065625da90 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -1778,7 +1778,7 @@ "TYPE": { "OWNED": "Сопствени проекти", "OWNED_SINGULAR": "Сопствен проект", - "GRANTED_SINGULAR": "Доделен проект" + "GRANTED_SINGULAR": "Проект доделен на {{name}}" }, "PRIVATELABEL": { "TITLE": "Подесувања за брендирање", diff --git a/console/src/assets/i18n/nl.json b/console/src/assets/i18n/nl.json index c2526c848d..9befa4746a 100644 --- a/console/src/assets/i18n/nl.json +++ b/console/src/assets/i18n/nl.json @@ -1777,7 +1777,7 @@ "TYPE": { "OWNED": "Eigen Projecten", "OWNED_SINGULAR": "Eigen Project", - "GRANTED_SINGULAR": "Verleend Project" + "GRANTED_SINGULAR": "Project toegewezen {{name}}" }, "PRIVATELABEL": { "TITLE": "Branding Instelling", diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index 65e13ce71f..ba5337ab6a 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -1776,7 +1776,7 @@ "TYPE": { "OWNED": "Własne Projekty", "OWNED_SINGULAR": "Własny Projekt", - "GRANTED_SINGULAR": "Udzielony Projekt" + "GRANTED_SINGULAR": "Projekt przydzielony {{name}}" }, "PRIVATELABEL": { "TITLE": "Ustawienia marka", diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json index aee0da5a02..905c454aca 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -1776,7 +1776,7 @@ "TYPE": { "OWNED": "Projetos Próprios", "OWNED_SINGULAR": "Projeto Próprio", - "GRANTED_SINGULAR": "Projeto Concedido" + "GRANTED_SINGULAR": "Projeto atribuído a {{name}}" }, "PRIVATELABEL": { "TITLE": "Configuração de Marca", diff --git a/console/src/assets/i18n/ru.json b/console/src/assets/i18n/ru.json index 294039adc9..894a5d4c6a 100644 --- a/console/src/assets/i18n/ru.json +++ b/console/src/assets/i18n/ru.json @@ -1858,9 +1858,8 @@ "ZITADELPROJECT": "Это принадлежит проекту ZITADEL. Осторожно: Если вы внесёте изменения, ZITADEL может вести себя не так, как предполагалось.", "TYPE": { "OWNED": "Собственные проекты", - "GRANTED": "Проекты доступа", "OWNED_SINGULAR": "Собственный проект", - "GRANTED_SINGULAR": "Допуск проекта" + "GRANTED_SINGULAR": "Проект, предоставленный {{name}}" }, "PRIVATELABEL": { "TITLE": "Настройка брендинга", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index c8b2baa0a1..3ffe728ae2 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -1775,7 +1775,7 @@ "TYPE": { "OWNED": "拥有的项目", "OWNED_SINGULAR": "拥有项目", - "GRANTED_SINGULAR": "被授予的项目" + "GRANTED_SINGULAR": "授予{{name}}的项目" }, "PRIVATELABEL": { "TITLE": "品牌标识设置", diff --git a/internal/notification/templates/templateData.go b/internal/notification/templates/templateData.go index f9572a4c5d..8ff750da53 100644 --- a/internal/notification/templates/templateData.go +++ b/internal/notification/templates/templateData.go @@ -2,7 +2,6 @@ package templates import ( "fmt" - "html" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/i18n" @@ -40,7 +39,7 @@ func (data *TemplateData) Translate(translator *i18n.Translator, msgType string, data.PreHeader = translator.Localize(fmt.Sprintf("%s.%s", msgType, domain.MessagePreHeader), args, langs...) data.Subject = translator.Localize(fmt.Sprintf("%s.%s", msgType, domain.MessageSubject), args, langs...) data.Greeting = translator.Localize(fmt.Sprintf("%s.%s", msgType, domain.MessageGreeting), args, langs...) - data.Text = html.UnescapeString(translator.Localize(fmt.Sprintf("%s.%s", msgType, domain.MessageText), args, langs...)) + data.Text = translator.Localize(fmt.Sprintf("%s.%s", msgType, domain.MessageText), args, langs...) data.ButtonText = translator.Localize(fmt.Sprintf("%s.%s", msgType, domain.MessageButtonText), args, langs...) // Footer text is neither included in i18n files nor defaults.yaml footerText := fmt.Sprintf("%s.%s", msgType, domain.MessageFooterText) diff --git a/internal/notification/types/notification.go b/internal/notification/types/notification.go index 1a5ccebb21..9bbfb66c8f 100644 --- a/internal/notification/types/notification.go +++ b/internal/notification/types/notification.go @@ -2,7 +2,9 @@ package types import ( "context" + "html" + "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/notification/channels/smtp" @@ -42,6 +44,7 @@ func SendEmail( allowUnverifiedNotificationChannel bool, ) error { args = mapNotifyUserToArgs(user, args) + sanitizeArgsForHTML(args) data := GetTemplateData(ctx, translator, args, url, messageType, user.PreferredLanguage.String(), colors) template, err := templates.GetParsedTemplate(mailhtml, data) if err != nil { @@ -59,6 +62,23 @@ func SendEmail( } } +func sanitizeArgsForHTML(args map[string]any) { + for key, arg := range args { + switch a := arg.(type) { + case string: + args[key] = html.EscapeString(a) + case []string: + for i, s := range a { + a[i] = html.EscapeString(s) + } + case database.TextArray[string]: + for i, s := range a { + a[i] = html.EscapeString(s) + } + } + } +} + func SendSMSTwilio( ctx context.Context, channels ChannelChains,