CVE-2023-41316: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Tolgee is an open-source localization platform. Due to lack of validation field - Org Name, bad actor can send emails with HTML injected code to the victims. Registered users can inject HTML into unsanitized emails from the Tolgee instance to other users. This unsanitized HTML ends up in invitation emails which appear as legitimate org invitations. Bad actors may direct users to malicious website or execute javascript in the context of the users browser. This vulnerability has been addressed in version 3.29.2. Users are advised to upgrade. There are no known workarounds for this vulnerability.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 5.4
Published: 2023-09-07T20:15Z
Repository: https://github.com/tolgee/tolgee-platform
Language: Kotlin
Files changed: backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/organizationController/OrganizationControllerInvitingTest.kt, backend/data/src/main/kotlin/io/tolgee/component/email/InvitationEmailSender.kt
Commit date: 2023-09-06 12:10:50 +0000
Commit message: fix: sanitize project/org name in invitation emails (#1898)


Diff: diff --git a/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/organizationController/OrganizationControllerInvitingTest.kt b/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/organizationController/OrganizationControllerInvitingTest.kt
index 428148540..9e79354bc 100644
--- a/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/organizationController/OrganizationControllerInvitingTest.kt
+++ b/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/organizationController/OrganizationControllerInvitingTest.kt
@@ -170,10 +170,23 @@ class OrganizationControllerInvitingTest : AuthorizedControllerTest() {
     assertThat(messageContent).contains(code)
     assertThat(messageContent).contains("http://localhost/")
     emailTestUtil.assertEmailTo.isEqualTo(INVITED_EMAIL)
   }
 
+  @Test
+  fun `e-mail is sanitized`() {
+    dummyDto.name = "Test org <a href='https://evil.local'>test</a>"
+    val organization = prepareTestOrganization()
+
+    inviteWithUserWithNameAndEmail(organization.id)
+    emailTestUtil.verifyEmailSent()
+
+    val messageContent = emailTestUtil.messageContents.single()
+    assertThat(messageContent).doesNotContain("<a href='https://evil.local")
+    assertThat(messageContent).contains("&lt;a href=&#39;https://evil.local")
+  }
+
   @Test
   fun `does not invite when email already invited`() {
     val organization = prepareTestOrganization()
     performCreateInvitation(organization.id).andIsOk
     performCreateInvitation(organization.id).andIsBadRequest
diff --git a/backend/data/src/main/kotlin/io/tolgee/component/email/InvitationEmailSender.kt b/backend/data/src/main/kotlin/io/tolgee/component/email/InvitationEmailSender.kt
index 5fa3bcb90..cdf976783 100644
--- a/backend/data/src/main/kotlin/io/tolgee/component/email/InvitationEmailSender.kt
+++ b/backend/data/src/main/kotlin/io/tolgee/component/email/InvitationEmailSender.kt
@@ -2,10 +2,11 @@ package io.tolgee.component.email
 
 import io.tolgee.component.FrontendUrlProvider
 import io.tolgee.dtos.misc.EmailParams
 import io.tolgee.model.Invitation
 import org.springframework.stereotype.Component
+import org.springframework.web.util.HtmlUtils
 
 @Component
 class InvitationEmailSender(
   private val tolgeeEmailSender: TolgeeEmailSender,
   private val frontendUrlProvider: FrontendUrlProvider
@@ -43,12 +44,14 @@ class InvitationEmailSender(
       organizationNameOrNull != null -> "organization"
       else -> throw IllegalStateException("No organization or project!")
     }
 
     val name = projectNameOrNull ?: organizationNameOrNull
+      ?: throw IllegalStateException("Both the organization and the project are null??")
 
-    return "You have been invited to $toWhat $name in Tolgee."
+    val escapedName = HtmlUtils.htmlEscape(name)
+    return "You have been invited to $toWhat $escapedName in Tolgee."
   }
 
   private fun getInvitationAcceptUrl(code: String): String {
     return "${frontendUrlProvider.url}/accept_invitation/$code"
   }

Vulnerable code:     return "You have been invited to $toWhat $name in Tolgee."
Fixed code:   @Test
  fun `e-mail is sanitized`() {
    dummyDto.name = "Test org <a href='https://evil.local'>test</a>"
    val organization = prepareTestOrganization()

    inviteWithUserWithNameAndEmail(organization.id)
    emailTestUtil.verifyEmailSent()

    val messageContent = emailTestUtil.messageContents.single()
    assertThat(messageContent).doesNotContain("<a href='https://evil.local")
    assertThat(messageContent).contains("&lt;a href=&#39;https://evil.local")
  }

import org.springframework.web.util.HtmlUtils
      ?: throw IllegalStateException("Both the organization and the project are null??")
    val escapedName = HtmlUtils.htmlEscape(name)
    return "You have been invited to $toWhat $escapedName in Tolgee."