Root Cause Analysis
The vulnerability is a result of improper neutralization of user-controlled input in various email notification functions. The root cause is that multiple ToMail methods within the models and handler packages were embedding raw, unescaped strings (such as task titles, project titles, and user names) into content that was later processed as Markdown. An attacker could set a task title, for example, to a string containing Markdown syntax like ](https://evil.com) [Click me. When Vikunja generated an overdue task notification, this malicious string would be concatenated into a Markdown link, breaking the intended structure and creating a new, malicious link. The application's Markdown renderer (goldmark) would then convert this to HTML, and the sanitizer (bluemonday) would allow the resulting <a> tag, as it permits links. This allowed an attacker with permission to edit task titles to send phishing links and tracking pixels in legitimate notification emails to other users. The fix involved creating and applying a new notifications.EscapeMarkdown function to all user-controlled strings before they are embedded in the email content, ensuring they are treated as literal text rather than active Markdown syntax.
Vulnerable functions
UndoneTasksOverdueNotification.ToMailpkg/models/notifications.go
This function constructs a Markdown list of overdue tasks for an email notification. It directly concatenates the raw `task.Title` and `n.Projects[task.ProjectID].Title` into the Markdown string. An attacker could provide a task title with malicious Markdown syntax, such as `](https://evil.com) [Click me`, to inject arbitrary links or images into the email sent to users, leading to phishing or tracking.
ReminderDueNotification.ToMailpkg/models/notifications.go
This function generates a reminder email and includes the task title (`n.Task.Title`) and project title (`n.Project.Title`) without escaping them. This allows for Markdown injection, which is then rendered into HTML, creating a cross-site scripting (XSS) vulnerability in the email client.
TaskAssignedNotification.ToMailpkg/models/notifications.go
When a task is assigned, this function creates an email notification. It includes the name of the user who assigned the task (`n.Doer.GetName()`) and the task title (`n.Task.Title`) without sanitization, making it vulnerable to Markdown injection.
TaskDeletedNotification.ToMailpkg/models/notifications.go
This function constructs an email for a deleted task, including the doer's name, task title, and task identifier. Since these values are not escaped, it is vulnerable to Markdown injection.
ProjectCreatedNotification.ToMailpkg/models/notifications.go
This function for project creation notifications includes the creator's name and the project title without escaping, allowing for Markdown injection.
TeamMemberAddedNotification.ToMailpkg/models/notifications.go
When a member is added to a team, this function sends a notification containing the doer's name and the team name. These values were not escaped, leading to a Markdown injection vulnerability.
UndoneTaskOverdueNotification.ToMailpkg/models/notifications.go
This function for single overdue task notifications includes the task and project titles without escaping, making it vulnerable to Markdown injection.
APITokenExpiringWeekNotification.ToMailpkg/models/api_tokens_expiry_notification.go
This function sends a notification about an expiring API token, including the token's title (`n.Token.Title`). Since the title is user-configurable and was not escaped, it could be used for Markdown injection.
APITokenExpiringDayNotification.ToMailpkg/models/api_tokens_expiry_notification.go
Similar to the weekly notification, this function for daily API token expiry notifications includes the unescaped token title, making it vulnerable to Markdown injection.
MigrationFailedNotification.ToMailpkg/modules/migration/handler/notifications.go
This function includes an error message in a notification. If the error message contains user-controlled input from an external data source during a migration, it could be a vector for Markdown injection. The patch proactively escapes this value.