Wagtail has improper permission handling on admin preview endpoints
Description
Wagtail is an open source content management system built on Django. Prior to versions 6.3.6, 7.0.4, 7.1.3, 7.2.2, and 7.3, due to a missing permission check on the preview endpoints, a user with access to the Wagtail admin and knowledge of a model's fields can craft a form submission to obtain a preview rendering of any page, snippet or site setting object for which previews are enabled, consisting of any data of the user's choosing. The existing data of the object itself is not exposed, but depending on the nature of the template being rendered, this may expose other database contents that would otherwise only be accessible to users with edit access over the model. 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.6, 7.0.4, 7.1.3, 7.2.2, and 7.3.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
wagtailPyPI | < 6.3.6 | 6.3.6 |
wagtailPyPI | >= 6.4rc1, < 7.0.4 | 7.0.4 |
wagtailPyPI | >= 7.1rc1, < 7.1.3 | 7.1.3 |
wagtailPyPI | >= 7.2rc1, < 7.2.2 | 7.2.2 |
wagtailPyPI | >= 7.3rc1, < 7.3 | 7.3 |
Affected products
1Patches
573f070dbefbdApply permission checks to page and snippet preview endpoints
4 files changed · +152 −2
wagtail/admin/tests/pages/test_preview.py+81 −0 modified@@ -1,6 +1,7 @@ import datetime from functools import wraps +from django.contrib.auth.models import Permission from django.test import TestCase, override_settings from django.urls import reverse from django.utils import timezone @@ -358,6 +359,47 @@ def test_preview_on_create_without_title_and_slug(self): self.assertTemplateUsed(response, "tests/event_page.html") self.assertContains(response, "Placeholder title") + def test_preview_on_create_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + preview_url = reverse( + "wagtailadmin_pages:preview_on_add", + args=("tests", "eventpage", self.home_page.id), + ) + + response = self.client.post( + preview_url, + self.post_data, + ) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_create_get_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + preview_url = reverse( + "wagtailadmin_pages:preview_on_add", + args=("tests", "eventpage", self.home_page.id), + ) + + response = self.client.get(preview_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + def test_preview_on_edit_with_m2m_field(self): preview_url = reverse( "wagtailadmin_pages:preview_on_edit", args=(self.event_page.id,) @@ -557,6 +599,45 @@ def test_preview_on_edit_expiry(self): response = self.client.get(preview_url) self.assertEqual(response.status_code, 200) + def test_preview_on_edit_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + preview_url = reverse( + "wagtailadmin_pages:preview_on_edit", args=(self.event_page.id,) + ) + + response = self.client.post( + preview_url, + self.post_data, + ) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_edit_get_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + preview_url = reverse( + "wagtailadmin_pages:preview_on_edit", args=(self.event_page.id,) + ) + + response = self.client.get(preview_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + def test_preview_on_create_clear_preview_data(self): preview_session_key = "wagtail-preview-tests-eventpage-{}".format( self.home_page.id
wagtail/admin/views/generic/preview.py+8 −1 modified@@ -17,13 +17,16 @@ from wagtail.models import PreviewableMixin, RevisionMixin from wagtail.utils.decorators import xframe_options_sameorigin_override +from .permissions import PermissionCheckedMixin -class PreviewOnEdit(View): + +class PreviewOnEdit(PermissionCheckedMixin, View): model = None form_class = None http_method_names = ("post", "get", "delete") preview_expiration_timeout = 60 * 60 * 24 # seconds session_key_prefix = "wagtail-preview-" + permission_required = "change" def setup(self, request, *args, **kwargs): super().setup(request, *args, **kwargs) @@ -54,6 +57,8 @@ def session_key(self): def get_object(self): obj = get_object_or_404(self.model, pk=unquote(str(self.kwargs["pk"]))) + if not self.user_has_permission_for_instance(self.permission_required, obj): + raise PermissionDenied if isinstance(obj, RevisionMixin): obj = obj.get_latest_revision_as_object() return obj @@ -133,6 +138,8 @@ def delete(self, request, *args, **kwargs): class PreviewOnCreate(PreviewOnEdit): + permission_required = "add" + @property def session_key(self): app_label = self.model._meta.app_label
wagtail/admin/views/pages/preview.py+10 −1 modified@@ -30,9 +30,13 @@ def session_key(self): return "{}{}".format(self.session_key_prefix, self.kwargs["page_id"]) def get_object(self): - return get_object_or_404( + page = get_object_or_404( Page, id=self.kwargs["page_id"] ).get_latest_revision_as_object() + page_perms = page.permissions_for_user(self.request.user) + if not page_perms.can_edit(): + raise PermissionDenied + return page def get_form(self, query_dict): form_class = self.object.get_edit_handler().get_form_class() @@ -87,6 +91,11 @@ def get_object(self): page = content_type.model_class()() parent_page = get_object_or_404(Page, id=parent_page_id).specific + + parent_page_perms = parent_page.permissions_for_user(self.request.user) + if not parent_page_perms.can_add_subpage(): + raise PermissionDenied + # We need to populate treebeard's path / depth fields in order to # pass validation. We can't make these 100% consistent with the rest # of the tree without making actual database changes (such as
wagtail/snippets/tests/test_preview.py+53 −0 modified@@ -1,5 +1,6 @@ import datetime +from django.contrib.auth.models import Permission from django.test import TestCase, override_settings from django.urls import reverse from django.utils import timezone @@ -145,6 +146,32 @@ def test_preview_on_create_with_deferred_required_fields(self): self.assertNotContains(response, "<li>Parties</li>") self.assertContains(response, "<li>Holidays</li>") + def test_preview_on_create_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + response = self.client.post(self.preview_on_add_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_create_get_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + response = self.client.get(self.preview_on_add_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + def test_preview_on_edit_with_m2m_field(self): response = self.client.post(self.preview_on_edit_url, self.post_data) @@ -262,6 +289,32 @@ def test_preview_on_edit_expiry(self): self.client.session, ) + def test_preview_on_edit_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + response = self.client.post(self.preview_on_edit_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_edit_get_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + response = self.client.get(self.preview_on_edit_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + def test_preview_on_create_clear_preview_data(self): # Set a fake preview session data for the page self.client.session[self.session_key_prefix] = "test data"
7dfe8de5f8b3Apply permission checks to page, snippet and site setting preview endpoints
6 files changed · +189 −4
wagtail/admin/tests/pages/test_preview.py+81 −0 modified@@ -1,6 +1,7 @@ import datetime from functools import wraps +from django.contrib.auth.models import Permission from django.test import TestCase, override_settings from django.urls import reverse from django.utils import timezone @@ -358,6 +359,47 @@ def test_preview_on_create_without_title_and_slug(self): self.assertTemplateUsed(response, "tests/event_page.html") self.assertContains(response, "Placeholder title") + def test_preview_on_create_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + preview_url = reverse( + "wagtailadmin_pages:preview_on_add", + args=("tests", "eventpage", self.home_page.id), + ) + + response = self.client.post( + preview_url, + self.post_data, + ) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_create_get_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + preview_url = reverse( + "wagtailadmin_pages:preview_on_add", + args=("tests", "eventpage", self.home_page.id), + ) + + response = self.client.get(preview_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + def test_preview_on_edit_with_m2m_field(self): preview_url = reverse( "wagtailadmin_pages:preview_on_edit", args=(self.event_page.id,) @@ -587,6 +629,45 @@ def test_preview_on_edit_expiry(self): response = self.client.get(preview_url) self.assertEqual(response.status_code, 200) + def test_preview_on_edit_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + preview_url = reverse( + "wagtailadmin_pages:preview_on_edit", args=(self.event_page.id,) + ) + + response = self.client.post( + preview_url, + self.post_data, + ) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_edit_get_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + preview_url = reverse( + "wagtailadmin_pages:preview_on_edit", args=(self.event_page.id,) + ) + + response = self.client.get(preview_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + def test_preview_on_create_clear_preview_data(self): preview_session_key = "wagtail-preview-tests-eventpage-{}".format( self.home_page.id
wagtail/admin/views/generic/preview.py+8 −1 modified@@ -17,13 +17,16 @@ from wagtail.models import PreviewableMixin, RevisionMixin from wagtail.utils.decorators import xframe_options_sameorigin_override +from .permissions import PermissionCheckedMixin -class PreviewOnEdit(View): + +class PreviewOnEdit(PermissionCheckedMixin, View): model = None form_class = None http_method_names = ("post", "get", "delete") preview_expiration_timeout = 60 * 60 * 24 # seconds session_key_prefix = "wagtail-preview-" + permission_required = "change" def setup(self, request, *args, **kwargs): super().setup(request, *args, **kwargs) @@ -54,6 +57,8 @@ def session_key(self): def get_object(self): obj = get_object_or_404(self.model, pk=unquote(str(self.kwargs["pk"]))) + if not self.user_has_permission_for_instance(self.permission_required, obj): + raise PermissionDenied if isinstance(obj, RevisionMixin): obj = obj.get_latest_revision_as_object() return obj @@ -136,6 +141,8 @@ def delete(self, request, *args, **kwargs): class PreviewOnCreate(PreviewOnEdit): + permission_required = "add" + @property def session_key(self): app_label = self.model._meta.app_label
wagtail/admin/views/pages/preview.py+10 −1 modified@@ -30,9 +30,13 @@ def session_key(self): return "{}{}".format(self.session_key_prefix, self.kwargs["page_id"]) def get_object(self): - return get_object_or_404( + page = get_object_or_404( Page, id=self.kwargs["page_id"] ).get_latest_revision_as_object() + page_perms = page.permissions_for_user(self.request.user) + if not page_perms.can_edit(): + raise PermissionDenied + return page def get_form(self, query_dict): form_class = self.object.get_edit_handler().get_form_class() @@ -87,6 +91,11 @@ def get_object(self): page = content_type.model_class()() parent_page = get_object_or_404(Page, id=parent_page_id).specific + + parent_page_perms = parent_page.permissions_for_user(self.request.user) + if not parent_page_perms.can_add_subpage(): + raise PermissionDenied + # We need to populate treebeard's path / depth fields in order to # pass validation. We can't make these 100% consistent with the rest # of the tree without making actual database changes (such as
wagtail/contrib/settings/tests/shared/test_preview.py+29 −0 modified@@ -1,3 +1,4 @@ +from django.contrib.auth.models import Permission from django.test import TestCase from django.urls import reverse @@ -118,6 +119,34 @@ def test_preview_on_edit_clear_preview_data(self): ) self.assertNotContains(response, versioned_static("wagtailadmin/js/icons.js")) + def test_preview_on_edit_without_permission(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + response = self.client.post(self.preview_on_edit_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_edit_get_without_permission(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + response = self.client.get(self.preview_on_edit_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + class TestSiteSettingPreview(TestGenericSiteSettingPreview): model = PreviewableSiteSetting
wagtail/contrib/settings/views.py+8 −2 modified@@ -205,16 +205,22 @@ def setup(self, request, app_name, model_name, *args, **kwargs): self.app_name = app_name self.model_name = model_name self.model = get_model_from_url_params(app_name, model_name) + self.permission_policy = self.model.get_permission_policy() self.pk = kwargs.get("pk") super().setup(request, app_name, model_name, *args, **kwargs) def get_object(self, queryset=None): self.site = None if issubclass(self.model, BaseSiteSetting): self.site = get_object_or_404(Site, pk=self.pk) - return self.model.for_site(self.site) + obj = self.model.for_site(self.site) else: - return get_object_or_404(self.model, pk=self.pk) + obj = get_object_or_404(self.model, pk=self.pk) + + if not self.user_has_permission_for_instance(self.permission_required, obj): + raise PermissionDenied + + return obj def get_extra_request_attrs(self): attrs = super().get_extra_request_attrs()
wagtail/snippets/tests/test_preview.py+53 −0 modified@@ -1,5 +1,6 @@ import datetime +from django.contrib.auth.models import Permission from django.test import TestCase, override_settings from django.urls import reverse from django.utils import timezone @@ -145,6 +146,32 @@ def test_preview_on_create_with_deferred_required_fields(self): self.assertNotContains(response, "<li>Parties</li>") self.assertContains(response, "<li>Holidays</li>") + def test_preview_on_create_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + response = self.client.post(self.preview_on_add_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_create_get_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + response = self.client.get(self.preview_on_add_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + def test_preview_on_edit_with_m2m_field(self): response = self.client.post(self.preview_on_edit_url, self.post_data) @@ -262,6 +289,32 @@ def test_preview_on_edit_expiry(self): self.client.session, ) + def test_preview_on_edit_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + response = self.client.post(self.preview_on_edit_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_edit_get_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + response = self.client.get(self.preview_on_edit_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + def test_preview_on_create_clear_preview_data(self): # Set a fake preview session data for the page self.client.session[self.session_key_prefix] = "test data"
01fd3477365aApply permission checks to page and snippet preview endpoints
4 files changed · +152 −2
wagtail/admin/tests/pages/test_preview.py+81 −0 modified@@ -1,6 +1,7 @@ import datetime from functools import wraps +from django.contrib.auth.models import Permission from django.test import TestCase, override_settings from django.urls import reverse from django.utils import timezone @@ -228,6 +229,47 @@ def test_preview_on_create_with_m2m_field(self): self.assertContains(response, "<li>Parties</li>") self.assertContains(response, "<li>Holidays</li>") + def test_preview_on_create_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + preview_url = reverse( + "wagtailadmin_pages:preview_on_add", + args=("tests", "eventpage", self.home_page.id), + ) + + response = self.client.post( + preview_url, + self.post_data, + ) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_create_get_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + preview_url = reverse( + "wagtailadmin_pages:preview_on_add", + args=("tests", "eventpage", self.home_page.id), + ) + + response = self.client.get(preview_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + def test_preview_on_edit_with_m2m_field(self): preview_url = reverse( "wagtailadmin_pages:preview_on_edit", args=(self.event_page.id,) @@ -318,6 +360,45 @@ def test_preview_on_edit_expiry(self): response = self.client.get(preview_url) self.assertEqual(response.status_code, 200) + def test_preview_on_edit_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + preview_url = reverse( + "wagtailadmin_pages:preview_on_edit", args=(self.event_page.id,) + ) + + response = self.client.post( + preview_url, + self.post_data, + ) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_edit_get_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + preview_url = reverse( + "wagtailadmin_pages:preview_on_edit", args=(self.event_page.id,) + ) + + response = self.client.get(preview_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + def test_preview_on_create_clear_preview_data(self): preview_session_key = "wagtail-preview-tests-eventpage-{}".format( self.home_page.id
wagtail/admin/views/generic/preview.py+8 −1 modified@@ -13,13 +13,16 @@ from wagtail.models import PreviewableMixin, RevisionMixin from wagtail.utils.decorators import xframe_options_sameorigin_override +from .permissions import PermissionCheckedMixin -class PreviewOnEdit(View): + +class PreviewOnEdit(PermissionCheckedMixin, View): model = None form_class = None http_method_names = ("post", "get", "delete") preview_expiration_timeout = 60 * 60 * 24 # seconds session_key_prefix = "wagtail-preview-" + permission_required = "change" def setup(self, request, *args, **kwargs): super().setup(request, *args, **kwargs) @@ -50,6 +53,8 @@ def session_key(self): def get_object(self): obj = get_object_or_404(self.model, pk=unquote(str(self.kwargs["pk"]))) + if not self.user_has_permission_for_instance(self.permission_required, obj): + raise PermissionDenied if isinstance(obj, RevisionMixin): obj = obj.get_latest_revision_as_object() return obj @@ -124,6 +129,8 @@ def delete(self, request, *args, **kwargs): class PreviewOnCreate(PreviewOnEdit): + permission_required = "add" + @property def session_key(self): app_label = self.model._meta.app_label
wagtail/admin/views/pages/preview.py+10 −1 modified@@ -27,9 +27,13 @@ def session_key(self): return "{}{}".format(self.session_key_prefix, self.kwargs["page_id"]) def get_object(self): - return get_object_or_404( + page = get_object_or_404( Page, id=self.kwargs["page_id"] ).get_latest_revision_as_object() + page_perms = page.permissions_for_user(self.request.user) + if not page_perms.can_edit(): + raise PermissionDenied + return page def get_form(self, query_dict): form_class = self.object.get_edit_handler().get_form_class() @@ -74,6 +78,11 @@ def get_object(self): page = content_type.model_class()() parent_page = get_object_or_404(Page, id=parent_page_id).specific + + parent_page_perms = parent_page.permissions_for_user(self.request.user) + if not parent_page_perms.can_add_subpage(): + raise PermissionDenied + # We need to populate treebeard's path / depth fields in order to # pass validation. We can't make these 100% consistent with the rest # of the tree without making actual database changes (such as
wagtail/snippets/tests/test_preview.py+53 −0 modified@@ -1,5 +1,6 @@ import datetime +from django.contrib.auth.models import Permission from django.test import TestCase, override_settings from django.urls import reverse from django.utils import timezone @@ -113,6 +114,32 @@ def test_preview_on_create_with_m2m_field(self): self.assertContains(response, "<li>Parties</li>") self.assertContains(response, "<li>Holidays</li>") + def test_preview_on_create_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + response = self.client.post(self.preview_on_add_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_create_get_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + response = self.client.get(self.preview_on_add_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + def test_preview_on_edit_with_m2m_field(self): response = self.client.post(self.preview_on_edit_url, self.post_data) @@ -200,6 +227,32 @@ def test_preview_on_edit_expiry(self): self.client.session, ) + def test_preview_on_edit_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + response = self.client.post(self.preview_on_edit_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_edit_get_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + response = self.client.get(self.preview_on_edit_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + def test_preview_on_create_clear_preview_data(self): # Set a fake preview session data for the page self.client.session[self.session_key_prefix] = "test data"
5f09b6da61e7Apply permission checks to page, snippet and site setting preview endpoints
6 files changed · +189 −4
wagtail/admin/tests/pages/test_preview.py+81 −0 modified@@ -1,6 +1,7 @@ import datetime from functools import wraps +from django.contrib.auth.models import Permission from django.test import TestCase, override_settings from django.urls import reverse from django.utils import timezone @@ -358,6 +359,47 @@ def test_preview_on_create_without_title_and_slug(self): self.assertTemplateUsed(response, "tests/event_page.html") self.assertContains(response, "Placeholder title") + def test_preview_on_create_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + preview_url = reverse( + "wagtailadmin_pages:preview_on_add", + args=("tests", "eventpage", self.home_page.id), + ) + + response = self.client.post( + preview_url, + self.post_data, + ) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_create_get_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + preview_url = reverse( + "wagtailadmin_pages:preview_on_add", + args=("tests", "eventpage", self.home_page.id), + ) + + response = self.client.get(preview_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + def test_preview_on_edit_with_m2m_field(self): preview_url = reverse( "wagtailadmin_pages:preview_on_edit", args=(self.event_page.id,) @@ -587,6 +629,45 @@ def test_preview_on_edit_expiry(self): response = self.client.get(preview_url) self.assertEqual(response.status_code, 200) + def test_preview_on_edit_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + preview_url = reverse( + "wagtailadmin_pages:preview_on_edit", args=(self.event_page.id,) + ) + + response = self.client.post( + preview_url, + self.post_data, + ) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_edit_get_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + preview_url = reverse( + "wagtailadmin_pages:preview_on_edit", args=(self.event_page.id,) + ) + + response = self.client.get(preview_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + def test_preview_on_create_clear_preview_data(self): preview_session_key = "wagtail-preview-tests-eventpage-{}".format( self.home_page.id
wagtail/admin/views/generic/preview.py+8 −1 modified@@ -17,13 +17,16 @@ from wagtail.models import PreviewableMixin, RevisionMixin from wagtail.utils.decorators import xframe_options_sameorigin_override +from .permissions import PermissionCheckedMixin -class PreviewOnEdit(View): + +class PreviewOnEdit(PermissionCheckedMixin, View): model = None form_class = None http_method_names = ("post", "get", "delete") preview_expiration_timeout = 60 * 60 * 24 # seconds session_key_prefix = "wagtail-preview-" + permission_required = "change" def setup(self, request, *args, **kwargs): super().setup(request, *args, **kwargs) @@ -54,6 +57,8 @@ def session_key(self): def get_object(self): obj = get_object_or_404(self.model, pk=unquote(str(self.kwargs["pk"]))) + if not self.user_has_permission_for_instance(self.permission_required, obj): + raise PermissionDenied if isinstance(obj, RevisionMixin): obj = obj.get_latest_revision_as_object() return obj @@ -136,6 +141,8 @@ def delete(self, request, *args, **kwargs): class PreviewOnCreate(PreviewOnEdit): + permission_required = "add" + @property def session_key(self): app_label = self.model._meta.app_label
wagtail/admin/views/pages/preview.py+10 −1 modified@@ -30,9 +30,13 @@ def session_key(self): return "{}{}".format(self.session_key_prefix, self.kwargs["page_id"]) def get_object(self): - return get_object_or_404( + page = get_object_or_404( Page, id=self.kwargs["page_id"] ).get_latest_revision_as_object() + page_perms = page.permissions_for_user(self.request.user) + if not page_perms.can_edit(): + raise PermissionDenied + return page def get_form(self, query_dict): form_class = self.object.get_edit_handler().get_form_class() @@ -87,6 +91,11 @@ def get_object(self): page = content_type.model_class()() parent_page = get_object_or_404(Page, id=parent_page_id).specific + + parent_page_perms = parent_page.permissions_for_user(self.request.user) + if not parent_page_perms.can_add_subpage(): + raise PermissionDenied + # We need to populate treebeard's path / depth fields in order to # pass validation. We can't make these 100% consistent with the rest # of the tree without making actual database changes (such as
wagtail/contrib/settings/tests/shared/test_preview.py+29 −0 modified@@ -1,3 +1,4 @@ +from django.contrib.auth.models import Permission from django.test import TestCase from django.urls import reverse @@ -118,6 +119,34 @@ def test_preview_on_edit_clear_preview_data(self): ) self.assertNotContains(response, versioned_static("wagtailadmin/js/icons.js")) + def test_preview_on_edit_without_permission(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + response = self.client.post(self.preview_on_edit_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_edit_get_without_permission(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + response = self.client.get(self.preview_on_edit_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + class TestSiteSettingPreview(TestGenericSiteSettingPreview): model = PreviewableSiteSetting
wagtail/contrib/settings/views.py+8 −2 modified@@ -198,16 +198,22 @@ def setup(self, request, app_name, model_name, *args, **kwargs): self.app_name = app_name self.model_name = model_name self.model = get_model_from_url_params(app_name, model_name) + self.permission_policy = self.model.get_permission_policy() self.pk = kwargs.get("pk") super().setup(request, app_name, model_name, *args, **kwargs) def get_object(self, queryset=None): self.site = None if issubclass(self.model, BaseSiteSetting): self.site = get_object_or_404(Site, pk=self.pk) - return self.model.for_site(self.site) + obj = self.model.for_site(self.site) else: - return get_object_or_404(self.model, pk=self.pk) + obj = get_object_or_404(self.model, pk=self.pk) + + if not self.user_has_permission_for_instance(self.permission_required, obj): + raise PermissionDenied + + return obj def get_extra_request_attrs(self): attrs = super().get_extra_request_attrs()
wagtail/snippets/tests/test_preview.py+53 −0 modified@@ -1,5 +1,6 @@ import datetime +from django.contrib.auth.models import Permission from django.test import TestCase, override_settings from django.urls import reverse from django.utils import timezone @@ -145,6 +146,32 @@ def test_preview_on_create_with_deferred_required_fields(self): self.assertNotContains(response, "<li>Parties</li>") self.assertContains(response, "<li>Holidays</li>") + def test_preview_on_create_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + response = self.client.post(self.preview_on_add_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_create_get_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + response = self.client.get(self.preview_on_add_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + def test_preview_on_edit_with_m2m_field(self): response = self.client.post(self.preview_on_edit_url, self.post_data) @@ -262,6 +289,32 @@ def test_preview_on_edit_expiry(self): self.client.session, ) + def test_preview_on_edit_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + response = self.client.post(self.preview_on_edit_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_edit_get_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + response = self.client.get(self.preview_on_edit_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + def test_preview_on_create_clear_preview_data(self): # Set a fake preview session data for the page self.client.session[self.session_key_prefix] = "test data"
dd824023a031Apply permission checks to page, snippet and site setting preview endpoints
6 files changed · +189 −4
wagtail/admin/tests/pages/test_preview.py+81 −0 modified@@ -1,6 +1,7 @@ import datetime from functools import wraps +from django.contrib.auth.models import Permission from django.test import TestCase, override_settings from django.urls import reverse from django.utils import timezone @@ -358,6 +359,47 @@ def test_preview_on_create_without_title_and_slug(self): self.assertTemplateUsed(response, "tests/event_page.html") self.assertContains(response, "Placeholder title") + def test_preview_on_create_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + preview_url = reverse( + "wagtailadmin_pages:preview_on_add", + args=("tests", "eventpage", self.home_page.id), + ) + + response = self.client.post( + preview_url, + self.post_data, + ) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_create_get_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + preview_url = reverse( + "wagtailadmin_pages:preview_on_add", + args=("tests", "eventpage", self.home_page.id), + ) + + response = self.client.get(preview_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + def test_preview_on_edit_with_m2m_field(self): preview_url = reverse( "wagtailadmin_pages:preview_on_edit", args=(self.event_page.id,) @@ -587,6 +629,45 @@ def test_preview_on_edit_expiry(self): response = self.client.get(preview_url) self.assertEqual(response.status_code, 200) + def test_preview_on_edit_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + preview_url = reverse( + "wagtailadmin_pages:preview_on_edit", args=(self.event_page.id,) + ) + + response = self.client.post( + preview_url, + self.post_data, + ) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_edit_get_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + preview_url = reverse( + "wagtailadmin_pages:preview_on_edit", args=(self.event_page.id,) + ) + + response = self.client.get(preview_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + def test_preview_on_create_clear_preview_data(self): preview_session_key = "wagtail-preview-tests-eventpage-{}".format( self.home_page.id
wagtail/admin/views/generic/preview.py+8 −1 modified@@ -17,13 +17,16 @@ from wagtail.models import PreviewableMixin, RevisionMixin from wagtail.utils.decorators import xframe_options_sameorigin_override +from .permissions import PermissionCheckedMixin -class PreviewOnEdit(View): + +class PreviewOnEdit(PermissionCheckedMixin, View): model = None form_class = None http_method_names = ("post", "get", "delete") preview_expiration_timeout = 60 * 60 * 24 # seconds session_key_prefix = "wagtail-preview-" + permission_required = "change" def setup(self, request, *args, **kwargs): super().setup(request, *args, **kwargs) @@ -54,6 +57,8 @@ def session_key(self): def get_object(self): obj = get_object_or_404(self.model, pk=unquote(str(self.kwargs["pk"]))) + if not self.user_has_permission_for_instance(self.permission_required, obj): + raise PermissionDenied if isinstance(obj, RevisionMixin): obj = obj.get_latest_revision_as_object() return obj @@ -136,6 +141,8 @@ def delete(self, request, *args, **kwargs): class PreviewOnCreate(PreviewOnEdit): + permission_required = "add" + @property def session_key(self): app_label = self.model._meta.app_label
wagtail/admin/views/pages/preview.py+10 −1 modified@@ -30,9 +30,13 @@ def session_key(self): return "{}{}".format(self.session_key_prefix, self.kwargs["page_id"]) def get_object(self): - return get_object_or_404( + page = get_object_or_404( Page, id=self.kwargs["page_id"] ).get_latest_revision_as_object() + page_perms = page.permissions_for_user(self.request.user) + if not page_perms.can_edit(): + raise PermissionDenied + return page def get_form(self, query_dict): form_class = self.object.get_edit_handler().get_form_class() @@ -87,6 +91,11 @@ def get_object(self): page = content_type.model_class()() parent_page = get_object_or_404(Page, id=parent_page_id).specific + + parent_page_perms = parent_page.permissions_for_user(self.request.user) + if not parent_page_perms.can_add_subpage(): + raise PermissionDenied + # We need to populate treebeard's path / depth fields in order to # pass validation. We can't make these 100% consistent with the rest # of the tree without making actual database changes (such as
wagtail/contrib/settings/tests/shared/test_preview.py+29 −0 modified@@ -1,3 +1,4 @@ +from django.contrib.auth.models import Permission from django.test import TestCase from django.urls import reverse @@ -118,6 +119,34 @@ def test_preview_on_edit_clear_preview_data(self): ) self.assertNotContains(response, versioned_static("wagtailadmin/js/icons.js")) + def test_preview_on_edit_without_permission(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + response = self.client.post(self.preview_on_edit_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_edit_get_without_permission(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + + response = self.client.get(self.preview_on_edit_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + class TestSiteSettingPreview(TestGenericSiteSettingPreview): model = PreviewableSiteSetting
wagtail/contrib/settings/views.py+8 −2 modified@@ -198,16 +198,22 @@ def setup(self, request, app_name, model_name, *args, **kwargs): self.app_name = app_name self.model_name = model_name self.model = get_model_from_url_params(app_name, model_name) + self.permission_policy = self.model.get_permission_policy() self.pk = kwargs.get("pk") super().setup(request, app_name, model_name, *args, **kwargs) def get_object(self, queryset=None): self.site = None if issubclass(self.model, BaseSiteSetting): self.site = get_object_or_404(Site, pk=self.pk) - return self.model.for_site(self.site) + obj = self.model.for_site(self.site) else: - return get_object_or_404(self.model, pk=self.pk) + obj = get_object_or_404(self.model, pk=self.pk) + + if not self.user_has_permission_for_instance(self.permission_required, obj): + raise PermissionDenied + + return obj def get_extra_request_attrs(self): attrs = super().get_extra_request_attrs()
wagtail/snippets/tests/test_preview.py+53 −0 modified@@ -1,5 +1,6 @@ import datetime +from django.contrib.auth.models import Permission from django.test import TestCase, override_settings from django.urls import reverse from django.utils import timezone @@ -145,6 +146,32 @@ def test_preview_on_create_with_deferred_required_fields(self): self.assertNotContains(response, "<li>Parties</li>") self.assertContains(response, "<li>Holidays</li>") + def test_preview_on_create_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + response = self.client.post(self.preview_on_add_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_create_get_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + response = self.client.get(self.preview_on_add_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + def test_preview_on_edit_with_m2m_field(self): response = self.client.post(self.preview_on_edit_url, self.post_data) @@ -262,6 +289,32 @@ def test_preview_on_edit_expiry(self): self.client.session, ) + def test_preview_on_edit_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + response = self.client.post(self.preview_on_edit_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_preview_on_edit_get_without_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + self.user.save() + response = self.client.get(self.preview_on_edit_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + def test_preview_on_create_clear_preview_data(self): # Set a fake preview session data for the page self.client.session[self.session_key_prefix] = "test data"
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
13- github.com/advisories/GHSA-4qvv-g3vr-m348ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-25517ghsaADVISORY
- github.com/wagtail/wagtail/commit/01fd3477365a193e6a8270311defb76e890d2719ghsax_refsource_MISCWEB
- github.com/wagtail/wagtail/commit/5f09b6da61e779b0e8499bdbba52bf2f7bd3241fghsax_refsource_MISCWEB
- github.com/wagtail/wagtail/commit/73f070dbefbd3b39ea6649ce36bd2d2a6eef2190ghsax_refsource_MISCWEB
- github.com/wagtail/wagtail/commit/7dfe8de5f8b3f112c73c87b6729197db16454915ghsax_refsource_MISCWEB
- github.com/wagtail/wagtail/commit/dd824023a031f1b82a6b6f83a97a5c73391b7c03ghsax_refsource_MISCWEB
- github.com/wagtail/wagtail/releases/tag/v6.3.6ghsaWEB
- github.com/wagtail/wagtail/releases/tag/v7.0.4ghsaWEB
- github.com/wagtail/wagtail/releases/tag/v7.1.3ghsaWEB
- github.com/wagtail/wagtail/releases/tag/v7.2.2ghsaWEB
- github.com/wagtail/wagtail/releases/tag/v7.3ghsaWEB
- github.com/wagtail/wagtail/security/advisories/GHSA-4qvv-g3vr-m348ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.