From c1a3fc72dde16e987d8a09aa291e7c2edfc928f7 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/sv.json | 2 +- console/src/assets/i18n/zh.json | 2 +- .../notification/templates/templateData.go | 3 +-- internal/notification/types/notification.go | 20 +++++++++++++++++++ 20 files changed, 39 insertions(+), 23 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 65e68dec7a4..7ef4c27e1d1 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 a86282326f0..4815cefef40 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 c04e428d050..17a41cce91c 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 0e4e9590bf0..0b57dee8fd2 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -1780,7 +1780,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 b16155886b2..75e65804d21 100644 --- a/console/src/assets/i18n/cs.json +++ b/console/src/assets/i18n/cs.json @@ -1782,7 +1782,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 1b717427ec7..0c823eb7853 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -1781,7 +1781,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 96a6de2a921..b707c19895c 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -1781,7 +1781,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 a6b3e0ba310..5b8f7f71da3 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -1782,7 +1782,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 8ed44fa2413..a277a56b25d 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -1781,7 +1781,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 c77ed216f01..eb54469ad45 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -1781,7 +1781,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 c0865167f52..3523a8f578b 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -1777,7 +1777,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 496f06d6417..11c5a422868 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -1782,7 +1782,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 6fa179e71dc..dff0401a052 100644 --- a/console/src/assets/i18n/nl.json +++ b/console/src/assets/i18n/nl.json @@ -1781,7 +1781,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 a7005564d69..43eab7ea43f 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -1780,7 +1780,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 79aa5090559..605322eb706 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -1780,7 +1780,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 56e538b90be..b2a3c528574 100644 --- a/console/src/assets/i18n/ru.json +++ b/console/src/assets/i18n/ru.json @@ -1862,9 +1862,8 @@ "ZITADELPROJECT": "Это принадлежит проекту ZITADEL. Осторожно: Если вы внесёте изменения, ZITADEL может вести себя не так, как предполагалось.", "TYPE": { "OWNED": "Собственные проекты", - "GRANTED": "Проекты доступа", "OWNED_SINGULAR": "Собственный проект", - "GRANTED_SINGULAR": "Допуск проекта" + "GRANTED_SINGULAR": "Проект, предоставленный {{name}}" }, "PRIVATELABEL": { "TITLE": "Настройка брендинга", diff --git a/console/src/assets/i18n/sv.json b/console/src/assets/i18n/sv.json index 3a15595b422..62e260fe16f 100644 --- a/console/src/assets/i18n/sv.json +++ b/console/src/assets/i18n/sv.json @@ -1781,7 +1781,7 @@ "TYPE": { "OWNED": "Ägda Projekt", "OWNED_SINGULAR": "Ägt Projekt", - "GRANTED_SINGULAR": "Beviljat Projekt" + "GRANTED_SINGULAR": "Projekt tilldelat {{name}}" }, "PRIVATELABEL": { "TITLE": "Varumärkesinställning", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index c35be2b6f52..2d308ec5baa 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -1780,7 +1780,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 f9572a4c5d1..8ff750da53a 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 1a5ccebb21a..9bbfb66c8f1 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,