From 189505c80fa639108488f5979fe52967df9729fa 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 --- .../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 65e68dec7..7ef4c27e1 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 a86282326..4815cefef 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 c04e428d0..17a41cce9 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 816b9fca0..920c5a604 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -1781,7 +1781,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 82896ce74..579ccbf26 100644 --- a/console/src/assets/i18n/cs.json +++ b/console/src/assets/i18n/cs.json @@ -1783,7 +1783,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 f17835beb..e07c00349 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -1782,7 +1782,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 dfacc1297..6fbfa6a6b 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -1782,7 +1782,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 56e310c18..a252d8db0 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -1783,7 +1783,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 f91de2aff..a4ff97869 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -1782,7 +1782,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 770d3840d..4693af618 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -1782,7 +1782,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 11c2ae1f6..fd823d524 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -1778,7 +1778,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 6edd41879..47b6f76ac 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -1783,7 +1783,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 a9b8e2039..f40fb493f 100644 --- a/console/src/assets/i18n/nl.json +++ b/console/src/assets/i18n/nl.json @@ -1782,7 +1782,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 75a3f0902..782327179 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -1781,7 +1781,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 f98bb7feb..ec2752086 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -1781,7 +1781,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 a12954f45..d0d17758f 100644 --- a/console/src/assets/i18n/ru.json +++ b/console/src/assets/i18n/ru.json @@ -1863,9 +1863,8 @@ "ZITADELPROJECT": "Это принадлежит проекту ZITADEL. Осторожно: Если вы внесёте изменения, ZITADEL может вести себя не так, как предполагалось.", "TYPE": { "OWNED": "Собственные проекты", - "GRANTED": "Проекты доступа", "OWNED_SINGULAR": "Собственный проект", - "GRANTED_SINGULAR": "Допуск проекта" + "GRANTED_SINGULAR": "Проект, предоставленный {{name}}" }, "PRIVATELABEL": { "0": { diff --git a/console/src/assets/i18n/sv.json b/console/src/assets/i18n/sv.json index 16c5fa16b..10b0b8615 100644 --- a/console/src/assets/i18n/sv.json +++ b/console/src/assets/i18n/sv.json @@ -1786,7 +1786,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 14226f8ec..27b695e8a 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -1781,7 +1781,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 f9572a4c5..8ff750da5 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 1a5ccebb2..9bbfb66c8 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,