diff --git a/openedx/core/djangoapps/notifications/base_notification.py b/openedx/core/djangoapps/notifications/base_notification.py index e47486df32e4..299542e1d03c 100644 --- a/openedx/core/djangoapps/notifications/base_notification.py +++ b/openedx/core/djangoapps/notifications/base_notification.py @@ -1,6 +1,7 @@ """ Base setup for Notification Apps and Types. """ +from django.utils.html import escape from django.utils.translation import gettext_lazy as _ from .email_notifications import EmailCadence @@ -11,6 +12,13 @@ FILTER_AUDIT_EXPIRED_USERS_WITH_NO_ROLE = 'filter_audit_expired_users_with_no_role' +# Context keys whose values are used as HTML tag names by content_templates +# (e.g. `<{p}>...<{strong}>{post_title}`). These must NOT be +# HTML-escaped before `template.format(**context)`; every other context value +# must be, since it typically comes from user input (thread title, username, +# etc.). See get_notification_content below. +_STRUCTURAL_CONTEXT_KEYS = frozenset({'p', 'strong'}) + COURSE_NOTIFICATION_TYPES = { 'new_comment_on_response': { 'notification_app': 'discussion', @@ -524,8 +532,16 @@ def get_notification_content(notification_type, context): context = context_function(context) if template: - # Handle grouped templates differently by modifying the context using a different function. - return template.format(**context) + # HTML-escape every context value except the structural tag-name + # keys, so that user-controlled input (post_title, replier_name, + # etc.) cannot inject `evil', '<style>body{background:red}</style>evil'), + ('', '<script>alert(1)</script>'), + ('AT&T "quoted"', 'AT&T "quoted"'), + ], +) +def test_get_notification_content_escapes_user_input(user_input, escaped): + """ + Regression test for GHSA-rv5w-f4r5-h77g: user-controlled context values + must be HTML-escaped before being interpolated into a content_template + via `str.format`. Structural context keys (`p`, `strong`) are exempt so + the template can still emit real

/ tags. + """ + context = {'replier_name': 'alice', 'post_title': user_input} + content = base_notification.get_notification_content('new_response', context) + assert '