Skip to content

Commit 21379e3

Browse files
committed
fix: escape task and language names in notification emails
Task notification emails interpolated the task name and the target language name straight into the HTML body. Both are user-supplied, so a project member could inject arbitrary markup into the mail every other member of the project receives.
1 parent 10d26c8 commit 21379e3

2 files changed

Lines changed: 57 additions & 1 deletion

File tree

backend/data/src/main/kotlin/io/tolgee/service/notification/TaskEmailComposer.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import io.tolgee.model.notifications.Notification
55
import io.tolgee.model.task.Task
66
import io.tolgee.util.I18n
77
import org.springframework.stereotype.Component
8+
import org.springframework.web.util.HtmlUtils
89

910
@Component
1011
class TaskEmailComposer(
@@ -33,7 +34,7 @@ class TaskEmailComposer(
3334
private fun taskLink(task: Task): String {
3435
return """
3536
|<a href="${taskUrl(task)}">
36-
| ${taskName(task.name)} #${task.number} (${task.language.name})
37+
| ${HtmlUtils.htmlEscape(taskName(task.name))} #${task.number} (${HtmlUtils.htmlEscape(task.language.name)})
3738
|</a>
3839
""".trimMargin()
3940
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package io.tolgee.unit
2+
3+
import io.tolgee.component.FrontendUrlProvider
4+
import io.tolgee.model.Language
5+
import io.tolgee.model.notifications.Notification
6+
import io.tolgee.model.notifications.NotificationType
7+
import io.tolgee.model.task.Task
8+
import io.tolgee.service.notification.TaskEmailComposer
9+
import io.tolgee.testing.assert
10+
import io.tolgee.util.I18n
11+
import org.junit.jupiter.api.Test
12+
import org.mockito.kotlin.any
13+
import org.mockito.kotlin.doReturn
14+
import org.mockito.kotlin.mock
15+
16+
class TaskEmailComposerTest {
17+
private val frontendUrlProvider =
18+
mock<FrontendUrlProvider> {
19+
on { getTaskUrl(any(), any()) } doReturn "https://app.tolgee.io/task"
20+
on { getMyTasksUrl() } doReturn "https://app.tolgee.io/my-tasks"
21+
}
22+
23+
private val composer = TaskEmailComposer(frontendUrlProvider, I18n())
24+
25+
@Test
26+
fun `escapes task name`() {
27+
val email = composer.composeEmail(notification(taskName = "<h1>pwned</h1>"))
28+
email.assert.doesNotContain("<h1>pwned</h1>")
29+
email.assert.contains("&lt;h1&gt;pwned&lt;/h1&gt;")
30+
}
31+
32+
@Test
33+
fun `escapes language name`() {
34+
val email = composer.composeEmail(notification(languageName = """<a href="https://evil.example">English</a>"""))
35+
email.assert.doesNotContain("""<a href="https://evil.example">""")
36+
email.assert.contains("&lt;a href=&quot;https://evil.example&quot;&gt;")
37+
}
38+
39+
private fun notification(
40+
taskName: String = "Translate",
41+
languageName: String = "English",
42+
): Notification {
43+
val task =
44+
Task().apply {
45+
this.name = taskName
46+
this.number = 1L
47+
this.language = Language().apply { this.name = languageName }
48+
}
49+
50+
return Notification().apply {
51+
this.type = NotificationType.TASK_ASSIGNED
52+
this.linkedTask = task
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)