VYPR
Moderate severityNVD Advisory· Published Mar 5, 2026· Updated Mar 6, 2026

Wagtail: Improper escaping of HTML (Cross-site Scripting) in simple_translation admin interface

CVE-2026-28223

Description

Wagtail is an open source content management system built on Django. Prior to versions 6.3.8, 7.0.6, 7.2.3, and 7.3.1, a stored cross-site scripting (XSS) vulnerability exists on confirmation messages within the wagtail.contrib.simple_translation module. A user with access to the Wagtail admin area may create a page with a specially-crafted title which, when another user performs the "Translate" action, causes arbitrary JavaScript code to run. This could lead to performing actions with that user's credentials. The vulnerability is not exploitable by an ordinary site visitor without access to the Wagtail admin. This issue has been patched in versions 6.3.8, 7.0.6, 7.2.3, and 7.3.1.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
wagtailPyPI
< 6.3.86.3.8
wagtailPyPI
>= 6.4rc1, < 7.0.67.0.6
wagtailPyPI
>= 7.1rc1, < 7.2.37.2.3
wagtailPyPI
>= 7.3rc1, < 7.3.17.3.1

Affected products

1

Patches

4
ba70244d376a

Enforce HTML escaping of all confirmation / warning / error messages

https://github.com/wagtail/wagtailMatthew WestcottFeb 24, 2026via ghsa
4 files changed · +81 12
  • wagtail/admin/templates/wagtailadmin/base.html+1 1 modified
    @@ -25,7 +25,7 @@
                                     {% else %}
                                         {% icon name=level_tag classname="messages-icon" %}
                                     {% endif %}
    -                                {{ message|safe }}
    +                                {{ message }}
                                     {% if level_tag == "error" %}
                                         {# Show a button to focus the first field/response error if available. #}
                                         <button
    
  • wagtail/admin/views/account.py+1 1 modified
    @@ -2,7 +2,6 @@
     from functools import cached_property
     
     from django.conf import settings
    -from django.contrib import messages
     from django.contrib.auth import update_session_auth_hash
     from django.contrib.auth import views as auth_views
     from django.db import transaction
    @@ -19,6 +18,7 @@
     from django.views.generic.base import TemplateView
     
     from wagtail import hooks
    +from wagtail.admin import messages
     from wagtail.admin.forms.account import (
         AvatarPreferencesForm,
         LocalePreferencesForm,
    
  • wagtail/contrib/simple_translation/tests/test_views.py+78 9 modified
    @@ -179,8 +179,24 @@ def test_submit_page_translation_view_test_post_single_locale(self):
             )
     
             self.assertIn(
    -            "The page 'Blog' was successfully created in German",
    -            [msg.message for msg in response.context["messages"]],
    +            "The page &#x27;Blog&#x27; was successfully created in German",
    +            [msg.message.strip() for msg in response.context["messages"]],
    +        )
    +
    +    def test_submit_page_translation_view_escapes_title_in_confirmation_message(self):
    +        self.en_blog_index.title = "<img src=x onerror=alert(4242)>"
    +        self.en_blog_index.draft_title = self.en_blog_index.title
    +        self.en_blog_index.save()
    +        url = reverse(
    +            "simple_translation:submit_page_translation", args=(self.en_blog_index.id,)
    +        )
    +        de = Locale.objects.get(language_code="de").id
    +        data = {"locales": [de], "include_subtree": True}
    +        self.login()
    +        response = self.client.post(url, data, follow=True)
    +        self.assertContains(
    +            response,
    +            "The page &#x27;&lt;img src=x onerror=alert(4242)&gt;&#x27; was successfully created in German",
             )
     
         def test_submit_page_translation_view_test_post_multiple_locales(self):
    @@ -208,10 +224,30 @@ def test_submit_page_translation_view_test_post_multiple_locales(self):
             assert response.url == f"/admin/pages/{self.en_blog_index.get_parent().id}/"
     
             response = self.client.get(response.url)  # follow the redirect
    -        assert [msg.message for msg in response.context["messages"]] == [
    -            "The page 'Blog' was successfully created in 2 locales"
    +        assert [msg.message.strip() for msg in response.context["messages"]] == [
    +            "The page &#x27;Blog&#x27; was successfully created in 2 locales"
             ]
     
    +    def test_submit_page_translation_view_multiple_locales_escapes_title_in_confirmation_message(
    +        self,
    +    ):
    +        self.en_blog_index.title = "<img src=x onerror=alert(4242)>"
    +        self.en_blog_index.draft_title = self.en_blog_index.title
    +        self.en_blog_index.save()
    +
    +        url = reverse(
    +            "simple_translation:submit_page_translation", args=(self.en_blog_index.id,)
    +        )
    +        de = Locale.objects.get(language_code="de").id
    +        fr = Locale.objects.get(language_code="fr").id
    +        data = {"locales": [de, fr], "include_subtree": True}
    +        self.login()
    +        response = self.client.post(url, data, follow=True)
    +        self.assertContains(
    +            response,
    +            "The page &#x27;&lt;img src=x onerror=alert(4242)&gt;&#x27; was successfully created in 2 locales",
    +        )
    +
     
     @override_settings(
         LANGUAGES=[
    @@ -359,7 +395,7 @@ def test_submit_snippet_translation_view_test_post_single_locale(self):
             translated_snippet = self.en_snippet.get_translation(self.de_locale.id)
             self.assertRedirects(response, self.get_snippet_url("edit", translated_snippet))
     
    -        self.assertContains(response, "It's edited", count=1)
    +        self.assertContains(response, "It&#x27;s edited")
             self.assertContains(response, '<h3 id="status-sidebar-german"', count=1)
             self.assertContains(
                 response,
    @@ -372,8 +408,24 @@ def test_submit_snippet_translation_view_test_post_single_locale(self):
             )
     
             self.assertEqual(
    -            [msg.message for msg in response.context["messages"]],
    -            ["Successfully created German for full-featured snippet 'It's edited'"],
    +            [msg.message.strip() for msg in response.context["messages"]],
    +            [
    +                "Successfully created German for full-featured snippet &#x27;It&#x27;s edited&#x27;"
    +            ],
    +        )
    +
    +    def test_submit_snippet_translation_view_escapes_title_in_confirmation_message(
    +        self,
    +    ):
    +        self.en_snippet.text = "<img src=x onerror=alert(4242)>"
    +        self.en_snippet.save_revision()
    +
    +        data = {"locales": [self.de_locale.id], "include_subtree": True}
    +        response = self.client.post(self.get_submit_url(), data, follow=True)
    +
    +        self.assertContains(
    +            response,
    +            "Successfully created German for full-featured snippet &#x27;&lt;img src=x onerror=alert(4242)&gt;&#x27;",
             )
     
         def test_submit_snippet_translation_view_test_post_multiple_locales(self):
    @@ -387,8 +439,25 @@ def test_submit_snippet_translation_view_test_post_multiple_locales(self):
     
             response = self.client.get(response.url)  # follow the redirect
             self.assertEqual(
    -            [msg.message for msg in response.context["messages"]],
    -            ["Successfully created 2 locales for full-featured snippet 'It's edited'"],
    +            [msg.message.strip() for msg in response.context["messages"]],
    +            [
    +                "Successfully created 2 locales for full-featured snippet &#x27;It&#x27;s edited&#x27;"
    +            ],
    +        )
    +
    +    def test_submit_snippet_translation_view_multiple_locales_escapes_title_in_confirmation_message(
    +        self,
    +    ):
    +        self.en_snippet.text = "<img src=x onerror=alert(4242)>"
    +        self.en_snippet.save_revision()
    +
    +        url = self.get_submit_url()
    +        data = {"locales": [self.de_locale.id, self.fr_locale.id]}
    +        response = self.client.post(url, data, follow=True)
    +
    +        self.assertContains(
    +            response,
    +            "Successfully created 2 locales for full-featured snippet &#x27;&lt;img src=x onerror=alert(4242)&gt;&#x27;",
             )
     
     
    
  • wagtail/contrib/simple_translation/views.py+1 1 modified
    @@ -1,4 +1,3 @@
    -from django.contrib import messages
     from django.contrib.admin.utils import unquote
     from django.core.exceptions import PermissionDenied
     from django.db import transaction
    @@ -11,6 +10,7 @@
     from django.views.generic.detail import SingleObjectMixin
     
     from wagtail.actions.copy_for_translation import CopyPageForTranslationAction
    +from wagtail.admin import messages
     from wagtail.models import DraftStateMixin, Page, TranslatableMixin
     from wagtail.snippets.views.snippets import get_snippet_model_from_url_params
     
    
1c6f2effed68

Enforce HTML escaping of all confirmation / warning / error messages

https://github.com/wagtail/wagtailMatthew WestcottFeb 24, 2026via ghsa
4 files changed · +81 12
  • wagtail/admin/templates/wagtailadmin/base.html+1 1 modified
    @@ -25,7 +25,7 @@
                                     {% else %}
                                         {% icon name=level_tag classname="messages-icon" %}
                                     {% endif %}
    -                                {{ message|safe }}
    +                                {{ message }}
                                     {% if level_tag == "error" %}
                                         {# Show a button to focus the first field/response error if available. #}
                                         <button
    
  • wagtail/admin/views/account.py+1 1 modified
    @@ -2,7 +2,6 @@
     from functools import cached_property
     
     from django.conf import settings
    -from django.contrib import messages
     from django.contrib.auth import update_session_auth_hash
     from django.contrib.auth import views as auth_views
     from django.db import transaction
    @@ -19,6 +18,7 @@
     from django.views.generic.base import TemplateView
     
     from wagtail import hooks
    +from wagtail.admin import messages
     from wagtail.admin.forms.account import (
         AvatarPreferencesForm,
         LocalePreferencesForm,
    
  • wagtail/contrib/simple_translation/tests/test_views.py+78 9 modified
    @@ -179,8 +179,24 @@ def test_submit_page_translation_view_test_post_single_locale(self):
             )
     
             self.assertIn(
    -            "The page 'Blog' was successfully created in German",
    -            [msg.message for msg in response.context["messages"]],
    +            "The page &#x27;Blog&#x27; was successfully created in German",
    +            [msg.message.strip() for msg in response.context["messages"]],
    +        )
    +
    +    def test_submit_page_translation_view_escapes_title_in_confirmation_message(self):
    +        self.en_blog_index.title = "<img src=x onerror=alert(4242)>"
    +        self.en_blog_index.draft_title = self.en_blog_index.title
    +        self.en_blog_index.save()
    +        url = reverse(
    +            "simple_translation:submit_page_translation", args=(self.en_blog_index.id,)
    +        )
    +        de = Locale.objects.get(language_code="de").id
    +        data = {"locales": [de], "include_subtree": True}
    +        self.login()
    +        response = self.client.post(url, data, follow=True)
    +        self.assertContains(
    +            response,
    +            "The page &#x27;&lt;img src=x onerror=alert(4242)&gt;&#x27; was successfully created in German",
             )
     
         def test_submit_page_translation_view_test_post_multiple_locales(self):
    @@ -208,10 +224,30 @@ def test_submit_page_translation_view_test_post_multiple_locales(self):
             assert response.url == f"/admin/pages/{self.en_blog_index.get_parent().id}/"
     
             response = self.client.get(response.url)  # follow the redirect
    -        assert [msg.message for msg in response.context["messages"]] == [
    -            "The page 'Blog' was successfully created in 2 locales"
    +        assert [msg.message.strip() for msg in response.context["messages"]] == [
    +            "The page &#x27;Blog&#x27; was successfully created in 2 locales"
             ]
     
    +    def test_submit_page_translation_view_multiple_locales_escapes_title_in_confirmation_message(
    +        self,
    +    ):
    +        self.en_blog_index.title = "<img src=x onerror=alert(4242)>"
    +        self.en_blog_index.draft_title = self.en_blog_index.title
    +        self.en_blog_index.save()
    +
    +        url = reverse(
    +            "simple_translation:submit_page_translation", args=(self.en_blog_index.id,)
    +        )
    +        de = Locale.objects.get(language_code="de").id
    +        fr = Locale.objects.get(language_code="fr").id
    +        data = {"locales": [de, fr], "include_subtree": True}
    +        self.login()
    +        response = self.client.post(url, data, follow=True)
    +        self.assertContains(
    +            response,
    +            "The page &#x27;&lt;img src=x onerror=alert(4242)&gt;&#x27; was successfully created in 2 locales",
    +        )
    +
     
     @override_settings(
         LANGUAGES=[
    @@ -359,7 +395,7 @@ def test_submit_snippet_translation_view_test_post_single_locale(self):
             translated_snippet = self.en_snippet.get_translation(self.de_locale.id)
             self.assertRedirects(response, self.get_snippet_url("edit", translated_snippet))
     
    -        self.assertContains(response, "It's edited", count=1)
    +        self.assertContains(response, "It&#x27;s edited")
             self.assertContains(response, '<h3 id="status-sidebar-german"', count=1)
             self.assertContains(
                 response,
    @@ -372,8 +408,24 @@ def test_submit_snippet_translation_view_test_post_single_locale(self):
             )
     
             self.assertEqual(
    -            [msg.message for msg in response.context["messages"]],
    -            ["Successfully created German for full-featured snippet 'It's edited'"],
    +            [msg.message.strip() for msg in response.context["messages"]],
    +            [
    +                "Successfully created German for full-featured snippet &#x27;It&#x27;s edited&#x27;"
    +            ],
    +        )
    +
    +    def test_submit_snippet_translation_view_escapes_title_in_confirmation_message(
    +        self,
    +    ):
    +        self.en_snippet.text = "<img src=x onerror=alert(4242)>"
    +        self.en_snippet.save_revision()
    +
    +        data = {"locales": [self.de_locale.id], "include_subtree": True}
    +        response = self.client.post(self.get_submit_url(), data, follow=True)
    +
    +        self.assertContains(
    +            response,
    +            "Successfully created German for full-featured snippet &#x27;&lt;img src=x onerror=alert(4242)&gt;&#x27;",
             )
     
         def test_submit_snippet_translation_view_test_post_multiple_locales(self):
    @@ -387,8 +439,25 @@ def test_submit_snippet_translation_view_test_post_multiple_locales(self):
     
             response = self.client.get(response.url)  # follow the redirect
             self.assertEqual(
    -            [msg.message for msg in response.context["messages"]],
    -            ["Successfully created 2 locales for full-featured snippet 'It's edited'"],
    +            [msg.message.strip() for msg in response.context["messages"]],
    +            [
    +                "Successfully created 2 locales for full-featured snippet &#x27;It&#x27;s edited&#x27;"
    +            ],
    +        )
    +
    +    def test_submit_snippet_translation_view_multiple_locales_escapes_title_in_confirmation_message(
    +        self,
    +    ):
    +        self.en_snippet.text = "<img src=x onerror=alert(4242)>"
    +        self.en_snippet.save_revision()
    +
    +        url = self.get_submit_url()
    +        data = {"locales": [self.de_locale.id, self.fr_locale.id]}
    +        response = self.client.post(url, data, follow=True)
    +
    +        self.assertContains(
    +            response,
    +            "Successfully created 2 locales for full-featured snippet &#x27;&lt;img src=x onerror=alert(4242)&gt;&#x27;",
             )
     
     
    
  • wagtail/contrib/simple_translation/views.py+1 1 modified
    @@ -1,4 +1,3 @@
    -from django.contrib import messages
     from django.contrib.admin.utils import unquote
     from django.core.exceptions import PermissionDenied
     from django.db import transaction
    @@ -11,6 +10,7 @@
     from django.views.generic.detail import SingleObjectMixin
     
     from wagtail.actions.copy_for_translation import CopyPageForTranslationAction
    +from wagtail.admin import messages
     from wagtail.models import DraftStateMixin, Page, TranslatableMixin
     from wagtail.snippets.views.snippets import get_snippet_model_from_url_params
     
    
d8c5900982df

Enforce HTML escaping of all confirmation / warning / error messages

https://github.com/wagtail/wagtailMatthew WestcottFeb 24, 2026via ghsa
4 files changed · +81 12
  • wagtail/admin/templates/wagtailadmin/base.html+1 1 modified
    @@ -25,7 +25,7 @@
                                     {% else %}
                                         {% icon name=level_tag classname="messages-icon" %}
                                     {% endif %}
    -                                {{ message|safe }}
    +                                {{ message }}
                                 </li>
                             {% endfor %}
                         {% endif %}
    
  • wagtail/admin/views/account.py+1 1 modified
    @@ -2,7 +2,6 @@
     from functools import cached_property
     
     from django.conf import settings
    -from django.contrib import messages
     from django.contrib.auth import update_session_auth_hash
     from django.contrib.auth import views as auth_views
     from django.db import transaction
    @@ -19,6 +18,7 @@
     from django.views.generic.base import TemplateView
     
     from wagtail import hooks
    +from wagtail.admin import messages
     from wagtail.admin.forms.account import (
         AvatarPreferencesForm,
         LocalePreferencesForm,
    
  • wagtail/contrib/simple_translation/tests/test_views.py+78 9 modified
    @@ -179,8 +179,24 @@ def test_submit_page_translation_view_test_post_single_locale(self):
             )
     
             self.assertIn(
    -            "The page 'Blog' was successfully created in German",
    -            [msg.message for msg in response.context["messages"]],
    +            "The page &#x27;Blog&#x27; was successfully created in German",
    +            [msg.message.strip() for msg in response.context["messages"]],
    +        )
    +
    +    def test_submit_page_translation_view_escapes_title_in_confirmation_message(self):
    +        self.en_blog_index.title = "<img src=x onerror=alert(4242)>"
    +        self.en_blog_index.draft_title = self.en_blog_index.title
    +        self.en_blog_index.save()
    +        url = reverse(
    +            "simple_translation:submit_page_translation", args=(self.en_blog_index.id,)
    +        )
    +        de = Locale.objects.get(language_code="de").id
    +        data = {"locales": [de], "include_subtree": True}
    +        self.login()
    +        response = self.client.post(url, data, follow=True)
    +        self.assertContains(
    +            response,
    +            "The page &#x27;&lt;img src=x onerror=alert(4242)&gt;&#x27; was successfully created in German",
             )
     
         def test_submit_page_translation_view_test_post_multiple_locales(self):
    @@ -208,10 +224,30 @@ def test_submit_page_translation_view_test_post_multiple_locales(self):
             assert response.url == f"/admin/pages/{self.en_blog_index.get_parent().id}/"
     
             response = self.client.get(response.url)  # follow the redirect
    -        assert [msg.message for msg in response.context["messages"]] == [
    -            "The page 'Blog' was successfully created in 2 locales"
    +        assert [msg.message.strip() for msg in response.context["messages"]] == [
    +            "The page &#x27;Blog&#x27; was successfully created in 2 locales"
             ]
     
    +    def test_submit_page_translation_view_multiple_locales_escapes_title_in_confirmation_message(
    +        self,
    +    ):
    +        self.en_blog_index.title = "<img src=x onerror=alert(4242)>"
    +        self.en_blog_index.draft_title = self.en_blog_index.title
    +        self.en_blog_index.save()
    +
    +        url = reverse(
    +            "simple_translation:submit_page_translation", args=(self.en_blog_index.id,)
    +        )
    +        de = Locale.objects.get(language_code="de").id
    +        fr = Locale.objects.get(language_code="fr").id
    +        data = {"locales": [de, fr], "include_subtree": True}
    +        self.login()
    +        response = self.client.post(url, data, follow=True)
    +        self.assertContains(
    +            response,
    +            "The page &#x27;&lt;img src=x onerror=alert(4242)&gt;&#x27; was successfully created in 2 locales",
    +        )
    +
     
     @override_settings(
         LANGUAGES=[
    @@ -359,7 +395,7 @@ def test_submit_snippet_translation_view_test_post_single_locale(self):
             translated_snippet = self.en_snippet.get_translation(self.de_locale.id)
             self.assertRedirects(response, self.get_snippet_url("edit", translated_snippet))
     
    -        self.assertContains(response, "It's edited", count=1)
    +        self.assertContains(response, "It&#x27;s edited")
             self.assertContains(response, '<h3 id="status-sidebar-german"', count=1)
             self.assertContains(
                 response,
    @@ -372,8 +408,24 @@ def test_submit_snippet_translation_view_test_post_single_locale(self):
             )
     
             self.assertEqual(
    -            [msg.message for msg in response.context["messages"]],
    -            ["Successfully created German for full-featured snippet 'It's edited'"],
    +            [msg.message.strip() for msg in response.context["messages"]],
    +            [
    +                "Successfully created German for full-featured snippet &#x27;It&#x27;s edited&#x27;"
    +            ],
    +        )
    +
    +    def test_submit_snippet_translation_view_escapes_title_in_confirmation_message(
    +        self,
    +    ):
    +        self.en_snippet.text = "<img src=x onerror=alert(4242)>"
    +        self.en_snippet.save_revision()
    +
    +        data = {"locales": [self.de_locale.id], "include_subtree": True}
    +        response = self.client.post(self.get_submit_url(), data, follow=True)
    +
    +        self.assertContains(
    +            response,
    +            "Successfully created German for full-featured snippet &#x27;&lt;img src=x onerror=alert(4242)&gt;&#x27;",
             )
     
         def test_submit_snippet_translation_view_test_post_multiple_locales(self):
    @@ -387,8 +439,25 @@ def test_submit_snippet_translation_view_test_post_multiple_locales(self):
     
             response = self.client.get(response.url)  # follow the redirect
             self.assertEqual(
    -            [msg.message for msg in response.context["messages"]],
    -            ["Successfully created 2 locales for full-featured snippet 'It's edited'"],
    +            [msg.message.strip() for msg in response.context["messages"]],
    +            [
    +                "Successfully created 2 locales for full-featured snippet &#x27;It&#x27;s edited&#x27;"
    +            ],
    +        )
    +
    +    def test_submit_snippet_translation_view_multiple_locales_escapes_title_in_confirmation_message(
    +        self,
    +    ):
    +        self.en_snippet.text = "<img src=x onerror=alert(4242)>"
    +        self.en_snippet.save_revision()
    +
    +        url = self.get_submit_url()
    +        data = {"locales": [self.de_locale.id, self.fr_locale.id]}
    +        response = self.client.post(url, data, follow=True)
    +
    +        self.assertContains(
    +            response,
    +            "Successfully created 2 locales for full-featured snippet &#x27;&lt;img src=x onerror=alert(4242)&gt;&#x27;",
             )
     
     
    
  • wagtail/contrib/simple_translation/views.py+1 1 modified
    @@ -1,4 +1,3 @@
    -from django.contrib import messages
     from django.contrib.admin.utils import unquote
     from django.core.exceptions import PermissionDenied
     from django.db import transaction
    @@ -11,6 +10,7 @@
     from django.views.generic.detail import SingleObjectMixin
     
     from wagtail.actions.copy_for_translation import CopyPageForTranslationAction
    +from wagtail.admin import messages
     from wagtail.models import DraftStateMixin, Page, TranslatableMixin
     from wagtail.snippets.views.snippets import get_snippet_model_from_url_params
     
    
ee39d39deeb7

Enforce HTML escaping of all confirmation / warning / error messages

https://github.com/wagtail/wagtailMatthew WestcottFeb 24, 2026via ghsa
4 files changed · +81 12
  • wagtail/admin/templates/wagtailadmin/base.html+1 1 modified
    @@ -25,7 +25,7 @@
                                     {% else %}
                                         {% icon name=level_tag classname="messages-icon" %}
                                     {% endif %}
    -                                {{ message|safe }}
    +                                {{ message }}
                                 </li>
                             {% endfor %}
                         {% endif %}
    
  • wagtail/admin/views/account.py+1 1 modified
    @@ -2,7 +2,6 @@
     from functools import cached_property
     
     from django.conf import settings
    -from django.contrib import messages
     from django.contrib.auth import update_session_auth_hash
     from django.contrib.auth import views as auth_views
     from django.db import transaction
    @@ -19,6 +18,7 @@
     from django.views.generic.base import TemplateView
     
     from wagtail import hooks
    +from wagtail.admin import messages
     from wagtail.admin.forms.account import (
         AvatarPreferencesForm,
         LocalePreferencesForm,
    
  • wagtail/contrib/simple_translation/tests/test_views.py+78 9 modified
    @@ -179,8 +179,24 @@ def test_submit_page_translation_view_test_post_single_locale(self):
             )
     
             self.assertIn(
    -            "The page 'Blog' was successfully created in German",
    -            [msg.message for msg in response.context["messages"]],
    +            "The page &#x27;Blog&#x27; was successfully created in German",
    +            [msg.message.strip() for msg in response.context["messages"]],
    +        )
    +
    +    def test_submit_page_translation_view_escapes_title_in_confirmation_message(self):
    +        self.en_blog_index.title = "<img src=x onerror=alert(4242)>"
    +        self.en_blog_index.draft_title = self.en_blog_index.title
    +        self.en_blog_index.save()
    +        url = reverse(
    +            "simple_translation:submit_page_translation", args=(self.en_blog_index.id,)
    +        )
    +        de = Locale.objects.get(language_code="de").id
    +        data = {"locales": [de], "include_subtree": True}
    +        self.login()
    +        response = self.client.post(url, data, follow=True)
    +        self.assertContains(
    +            response,
    +            "The page &#x27;&lt;img src=x onerror=alert(4242)&gt;&#x27; was successfully created in German",
             )
     
         def test_submit_page_translation_view_test_post_multiple_locales(self):
    @@ -208,10 +224,30 @@ def test_submit_page_translation_view_test_post_multiple_locales(self):
             assert response.url == f"/admin/pages/{self.en_blog_index.get_parent().id}/"
     
             response = self.client.get(response.url)  # follow the redirect
    -        assert [msg.message for msg in response.context["messages"]] == [
    -            "The page 'Blog' was successfully created in 2 locales"
    +        assert [msg.message.strip() for msg in response.context["messages"]] == [
    +            "The page &#x27;Blog&#x27; was successfully created in 2 locales"
             ]
     
    +    def test_submit_page_translation_view_multiple_locales_escapes_title_in_confirmation_message(
    +        self,
    +    ):
    +        self.en_blog_index.title = "<img src=x onerror=alert(4242)>"
    +        self.en_blog_index.draft_title = self.en_blog_index.title
    +        self.en_blog_index.save()
    +
    +        url = reverse(
    +            "simple_translation:submit_page_translation", args=(self.en_blog_index.id,)
    +        )
    +        de = Locale.objects.get(language_code="de").id
    +        fr = Locale.objects.get(language_code="fr").id
    +        data = {"locales": [de, fr], "include_subtree": True}
    +        self.login()
    +        response = self.client.post(url, data, follow=True)
    +        self.assertContains(
    +            response,
    +            "The page &#x27;&lt;img src=x onerror=alert(4242)&gt;&#x27; was successfully created in 2 locales",
    +        )
    +
     
     @override_settings(
         LANGUAGES=[
    @@ -359,7 +395,7 @@ def test_submit_snippet_translation_view_test_post_single_locale(self):
             translated_snippet = self.en_snippet.get_translation(self.de_locale.id)
             self.assertRedirects(response, self.get_snippet_url("edit", translated_snippet))
     
    -        self.assertContains(response, "It's edited", count=1)
    +        self.assertContains(response, "It&#x27;s edited")
             self.assertContains(response, '<h3 id="status-sidebar-german"', count=1)
             self.assertContains(
                 response,
    @@ -372,8 +408,24 @@ def test_submit_snippet_translation_view_test_post_single_locale(self):
             )
     
             self.assertEqual(
    -            [msg.message for msg in response.context["messages"]],
    -            ["Successfully created German for full-featured snippet 'It's edited'"],
    +            [msg.message.strip() for msg in response.context["messages"]],
    +            [
    +                "Successfully created German for full-featured snippet &#x27;It&#x27;s edited&#x27;"
    +            ],
    +        )
    +
    +    def test_submit_snippet_translation_view_escapes_title_in_confirmation_message(
    +        self,
    +    ):
    +        self.en_snippet.text = "<img src=x onerror=alert(4242)>"
    +        self.en_snippet.save_revision()
    +
    +        data = {"locales": [self.de_locale.id], "include_subtree": True}
    +        response = self.client.post(self.get_submit_url(), data, follow=True)
    +
    +        self.assertContains(
    +            response,
    +            "Successfully created German for full-featured snippet &#x27;&lt;img src=x onerror=alert(4242)&gt;&#x27;",
             )
     
         def test_submit_snippet_translation_view_test_post_multiple_locales(self):
    @@ -387,8 +439,25 @@ def test_submit_snippet_translation_view_test_post_multiple_locales(self):
     
             response = self.client.get(response.url)  # follow the redirect
             self.assertEqual(
    -            [msg.message for msg in response.context["messages"]],
    -            ["Successfully created 2 locales for full-featured snippet 'It's edited'"],
    +            [msg.message.strip() for msg in response.context["messages"]],
    +            [
    +                "Successfully created 2 locales for full-featured snippet &#x27;It&#x27;s edited&#x27;"
    +            ],
    +        )
    +
    +    def test_submit_snippet_translation_view_multiple_locales_escapes_title_in_confirmation_message(
    +        self,
    +    ):
    +        self.en_snippet.text = "<img src=x onerror=alert(4242)>"
    +        self.en_snippet.save_revision()
    +
    +        url = self.get_submit_url()
    +        data = {"locales": [self.de_locale.id, self.fr_locale.id]}
    +        response = self.client.post(url, data, follow=True)
    +
    +        self.assertContains(
    +            response,
    +            "Successfully created 2 locales for full-featured snippet &#x27;&lt;img src=x onerror=alert(4242)&gt;&#x27;",
             )
     
     
    
  • wagtail/contrib/simple_translation/views.py+1 1 modified
    @@ -1,4 +1,3 @@
    -from django.contrib import messages
     from django.contrib.admin.utils import unquote
     from django.core.exceptions import PermissionDenied
     from django.db import transaction
    @@ -11,6 +10,7 @@
     from django.views.generic.detail import SingleObjectMixin
     
     from wagtail.actions.copy_for_translation import CopyPageForTranslationAction
    +from wagtail.admin import messages
     from wagtail.models import DraftStateMixin, Page, TranslatableMixin
     from wagtail.snippets.views.snippets import get_snippet_model_from_url_params
     
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

11

News mentions

0

No linked articles in our index yet.