VYPR
Moderate severityNVD Advisory· Published Feb 4, 2026· Updated Feb 5, 2026

Wagtail has improper permission handling on admin preview endpoints

CVE-2026-25517

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.

PackageAffected versionsPatched versions
wagtailPyPI
< 6.3.66.3.6
wagtailPyPI
>= 6.4rc1, < 7.0.47.0.4
wagtailPyPI
>= 7.1rc1, < 7.1.37.1.3
wagtailPyPI
>= 7.2rc1, < 7.2.27.2.2
wagtailPyPI
>= 7.3rc1, < 7.37.3

Affected products

1

Patches

5
73f070dbefbd

Apply permission checks to page and snippet preview endpoints

https://github.com/wagtail/wagtailMatthew WestcottJan 20, 2026via ghsa
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"
    
7dfe8de5f8b3

Apply permission checks to page, snippet and site setting preview endpoints

https://github.com/wagtail/wagtailMatthew WestcottJan 20, 2026via ghsa
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"
    
01fd3477365a

Apply permission checks to page and snippet preview endpoints

https://github.com/wagtail/wagtailMatthew WestcottJan 20, 2026via ghsa
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"
    
5f09b6da61e7

Apply permission checks to page, snippet and site setting preview endpoints

https://github.com/wagtail/wagtailMatthew WestcottJan 20, 2026via ghsa
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"
    
dd824023a031

Apply permission checks to page, snippet and site setting preview endpoints

https://github.com/wagtail/wagtailMatthew WestcottJan 20, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.