VYPR
Medium severity5.5NVD Advisory· Published May 30, 2024· Updated Apr 15, 2026

CVE-2024-35228

CVE-2024-35228

Description

Wagtail is an open source content management system built on Django. Due to an improperly applied permission check in the wagtail.contrib.settings module, a user with access to the Wagtail admin and knowledge of the URL of the edit view for a settings model can access and update that setting, even when they have not been granted permission over the model. The vulnerability is not exploitable by an ordinary site visitor without access to the Wagtail admin. Patched versions have been released as Wagtail 6.0.5 and 6.1.2. Wagtail releases prior to 6.0 are unaffected. Users are advised to upgrade. Site owners who are unable to upgrade to a patched version can avoid the vulnerability in ModelViewSet by registering the model as a snippet instead. No workaround is available for wagtail.contrib.settings.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
wagtailPyPI
>= 6.0.0, < 6.0.56.0.5
wagtailPyPI
>= 6.1.0, < 6.1.26.1.2

Patches

1
284f75a6f91f

Restore permission check on settings EditView

https://github.com/wagtail/wagtailMatt WestcottMay 22, 2024via ghsa
3 files changed · +289 33
  • wagtail/contrib/settings/tests/generic/test_admin.py+144 16 modified
    @@ -77,11 +77,6 @@ def edit_url(self, setting):
     class TestGenericSettingCreateView(BaseTestGenericSettingView):
         def setUp(self):
             self.user = self.login()
    -        self.user.user_permissions.add(
    -            Permission.objects.get(
    -                content_type__app_label="wagtailadmin", codename="access_admin"
    -            )
    -        )
     
         def test_get_edit(self):
             response = self.get()
    @@ -113,11 +108,62 @@ def test_file_upload_multipart(self):
             # Ensure the form supports file uploads
             self.assertContains(response, 'enctype="multipart/form-data"')
     
    -    def test_create_restricted_field_without_permission(self):
    +    def test_create_restricted_field_without_any_permission(self):
    +        # User has no permissions over the setting model, only access to the admin
             self.user.is_superuser = False
             self.user.save()
    +        self.user.user_permissions.add(
    +            Permission.objects.get(
    +                content_type__app_label="wagtailadmin", codename="access_admin"
    +            ),
    +        )
     
             self.assertFalse(TestPermissionedGenericSetting.objects.exists())
    +        # GET should redirect away with permission denied
    +        response = self.get(setting=TestPermissionedGenericSetting)
    +        self.assertRedirects(response, status_code=302, expected_url="/admin/")
    +
    +        # the GET might create a setting object, depending on when the permission check is done,
    +        # so remove any created objects prior to testing the POST
    +        TestPermissionedGenericSetting.objects.all().delete()
    +
    +        # POST should redirect away with permission denied
    +        response = self.post(
    +            post_data={"sensitive_email": "test@example.com", "title": "test"},
    +            setting=TestPermissionedGenericSetting,
    +        )
    +        self.assertRedirects(response, status_code=302, expected_url="/admin/")
    +
    +        # The retrieved setting should contain none of the submitted data
    +        setting = TestPermissionedGenericSetting.load()
    +        self.assertEqual(setting.title, "")
    +        self.assertEqual(setting.sensitive_email, "")
    +
    +    def test_create_restricted_field_without_field_permission(self):
    +        # User has edit permission over the setting model, but not the sensitive_email field
    +        self.user.is_superuser = False
    +        self.user.save()
    +        self.user.user_permissions.add(
    +            Permission.objects.get(
    +                content_type__app_label="wagtailadmin", codename="access_admin"
    +            ),
    +            Permission.objects.get(
    +                content_type__app_label="tests",
    +                codename="change_testpermissionedgenericsetting",
    +            ),
    +        )
    +
    +        self.assertFalse(TestPermissionedGenericSetting.objects.exists())
    +        # GET should provide a form with title but not sensitive_email
    +        response = self.get(setting=TestPermissionedGenericSetting)
    +        self.assertEqual(response.status_code, 200)
    +        self.assertIn("title", list(response.context["form"].fields))
    +        self.assertNotIn("sensitive_email", list(response.context["form"].fields))
    +
    +        # the GET creates a setting object, so remove any created objects prior to testing the POST
    +        TestPermissionedGenericSetting.objects.all().delete()
    +
    +        # POST should allow the title to be set, but not the sensitive_email
             response = self.post(
                 post_data={"sensitive_email": "test@example.com", "title": "test"},
                 setting=TestPermissionedGenericSetting,
    @@ -129,11 +175,31 @@ def test_create_restricted_field_without_permission(self):
             self.assertEqual(settings.sensitive_email, "")
     
         def test_create_restricted_field(self):
    +        # User has edit permission over the setting model, including the sensitive_email field
             self.user.is_superuser = False
             self.user.save()
             self.user.user_permissions.add(
    -            Permission.objects.get(codename="can_edit_sensitive_email_generic_setting")
    +            Permission.objects.get(
    +                content_type__app_label="wagtailadmin", codename="access_admin"
    +            ),
    +            Permission.objects.get(
    +                content_type__app_label="tests",
    +                codename="change_testpermissionedgenericsetting",
    +            ),
    +            Permission.objects.get(codename="can_edit_sensitive_email_generic_setting"),
             )
    +
    +        self.assertFalse(TestPermissionedGenericSetting.objects.exists())
    +        # GET should provide a form with title and sensitive_email
    +        response = self.get(setting=TestPermissionedGenericSetting)
    +        self.assertEqual(response.status_code, 200)
    +        self.assertIn("title", list(response.context["form"].fields))
    +        self.assertIn("sensitive_email", list(response.context["form"].fields))
    +
    +        # the GET creates a setting object, so remove any created objects prior to testing the POST
    +        TestPermissionedGenericSetting.objects.all().delete()
    +
    +        # POST should allow both title and sensitive_email to be set
             self.assertFalse(TestPermissionedGenericSetting.objects.exists())
             response = self.post(
                 post_data={"sensitive_email": "test@example.com", "title": "test"},
    @@ -153,11 +219,6 @@ def setUp(self):
             self.test_setting.save()
     
             self.user = self.login()
    -        self.user.user_permissions.add(
    -            Permission.objects.get(
    -                content_type__app_label="wagtailadmin", codename="access_admin"
    -            )
    -        )
     
         def test_get_edit(self):
             response = self.get()
    @@ -206,48 +267,115 @@ def test_for_request(self):
             )
     
         def test_edit_restricted_field(self):
    +        # User has edit permission over the setting model, including the sensitive_email field
             test_setting = TestPermissionedGenericSetting()
             test_setting.sensitive_email = "test@example.com"
    +        test_setting.title = "Old title"
             test_setting.save()
             self.user.is_superuser = False
             self.user.save()
     
             self.user.user_permissions.add(
    -            Permission.objects.get(codename="can_edit_sensitive_email_generic_setting")
    +            Permission.objects.get(
    +                content_type__app_label="wagtailadmin", codename="access_admin"
    +            ),
    +            Permission.objects.get(
    +                content_type__app_label="tests",
    +                codename="change_testpermissionedgenericsetting",
    +            ),
    +            Permission.objects.get(codename="can_edit_sensitive_email_generic_setting"),
             )
     
    +        # GET should provide a form with title and sensitive_email
             response = self.get(setting=TestPermissionedGenericSetting)
             self.assertEqual(response.status_code, 200)
    +        self.assertIn("title", list(response.context["form"].fields))
             self.assertIn("sensitive_email", list(response.context["form"].fields))
     
    +        # POST should allow both title and sensitive_email to be set
             response = self.post(
                 setting=TestPermissionedGenericSetting,
    -            post_data={"sensitive_email": "test-updated@example.com", "title": "title"},
    +            post_data={
    +                "sensitive_email": "test-updated@example.com",
    +                "title": "New title",
    +            },
             )
             self.assertEqual(response.status_code, 302)
     
             test_setting.refresh_from_db()
             self.assertEqual(test_setting.sensitive_email, "test-updated@example.com")
    +        self.assertEqual(test_setting.title, "New title")
     
    -    def test_edit_restricted_field_without_permission(self):
    +    def test_edit_restricted_field_without_field_permission(self):
    +        # User has edit permission over the setting model, but not the sensitive_email field
             test_setting = TestPermissionedGenericSetting()
             test_setting.sensitive_email = "test@example.com"
    +        test_setting.title = "Old title"
             test_setting.save()
             self.user.is_superuser = False
             self.user.save()
    +        self.user.user_permissions.add(
    +            Permission.objects.get(
    +                content_type__app_label="wagtailadmin", codename="access_admin"
    +            ),
    +            Permission.objects.get(
    +                content_type__app_label="tests",
    +                codename="change_testpermissionedgenericsetting",
    +            ),
    +        )
     
    +        # GET should provide a form with title but not sensitive_email
             response = self.get(setting=TestPermissionedGenericSetting)
             self.assertEqual(response.status_code, 200)
    +        self.assertIn("title", list(response.context["form"].fields))
             self.assertNotIn("sensitive_email", list(response.context["form"].fields))
     
    +        # POST should allow the title to be set, but not the sensitive_email
             response = self.post(
                 setting=TestPermissionedGenericSetting,
    -            post_data={"sensitive_email": "test-updated@example.com", "title": "title"},
    +            post_data={
    +                "sensitive_email": "test-updated@example.com",
    +                "title": "New title",
    +            },
             )
             self.assertEqual(response.status_code, 302)
     
             test_setting.refresh_from_db()
             self.assertEqual(test_setting.sensitive_email, "test@example.com")
    +        self.assertEqual(test_setting.title, "New title")
    +
    +    def test_edit_restricted_field_without_any_permission(self):
    +        # User has no permissions over the setting model, only access to the admin
    +        test_setting = TestPermissionedGenericSetting()
    +        test_setting.sensitive_email = "test@example.com"
    +        test_setting.title = "Old title"
    +        test_setting.save()
    +        self.user.is_superuser = False
    +        self.user.save()
    +        self.user.user_permissions.add(
    +            Permission.objects.get(
    +                content_type__app_label="wagtailadmin", codename="access_admin"
    +            ),
    +        )
    +
    +        # GET should redirect away with permission denied
    +        response = self.get(setting=TestPermissionedGenericSetting)
    +        self.assertRedirects(response, status_code=302, expected_url="/admin/")
    +
    +        # POST should redirect away with permission denied
    +        response = self.post(
    +            setting=TestPermissionedGenericSetting,
    +            post_data={
    +                "sensitive_email": "test-updated@example.com",
    +                "title": "new title",
    +            },
    +        )
    +        self.assertRedirects(response, status_code=302, expected_url="/admin/")
    +
    +        # The retrieved setting should be unchanged
    +        test_setting.refresh_from_db()
    +        self.assertEqual(test_setting.sensitive_email, "test@example.com")
    +        self.assertEqual(test_setting.title, "Old title")
     
     
     class TestAdminPermission(WagtailTestUtils, TestCase):
    
  • wagtail/contrib/settings/tests/site_specific/test_admin.py+142 17 modified
    @@ -73,11 +73,6 @@ def edit_url(self, setting, site_pk=1):
     class TestSiteSettingCreateView(BaseTestSiteSettingView):
         def setUp(self):
             self.user = self.login()
    -        self.user.user_permissions.add(
    -            Permission.objects.get(
    -                content_type__app_label="wagtailadmin", codename="access_admin"
    -            )
    -        )
     
         def test_get_edit(self):
             response = self.get()
    @@ -109,11 +104,61 @@ def test_file_upload_multipart(self):
             # Ensure the form supports file uploads
             self.assertContains(response, 'enctype="multipart/form-data"')
     
    -    def test_create_restricted_field_without_permission(self):
    +    def test_create_restricted_field_without_any_permission(self):
    +        # User has no permissions over the setting model, only access to the admin
    +        self.user.is_superuser = False
    +        self.user.save()
    +        self.user.user_permissions.add(
    +            Permission.objects.get(
    +                content_type__app_label="wagtailadmin", codename="access_admin"
    +            ),
    +        )
    +
    +        self.assertFalse(TestPermissionedSiteSetting.objects.exists())
    +        # GET should redirect away with permission denied
    +        response = self.get(setting=TestPermissionedSiteSetting)
    +        self.assertRedirects(response, status_code=302, expected_url="/admin/")
    +
    +        # the GET might create a setting object, depending on when the permission check is done,
    +        # so remove any created objects prior to testing the POST
    +
    +        # POST should redirect away with permission denied
    +        response = self.post(
    +            post_data={"sensitive_email": "test@example.com", "title": "test"},
    +            setting=TestPermissionedSiteSetting,
    +        )
    +        self.assertRedirects(response, status_code=302, expected_url="/admin/")
    +
    +        # The retrieved setting should contain none of the submitted data
    +        settings = TestPermissionedSiteSetting.for_site(Site.objects.get(pk=1))
    +        self.assertEqual(settings.title, "")
    +        self.assertEqual(settings.sensitive_email, "")
    +
    +    def test_create_restricted_field_without_field_permission(self):
    +        # User has edit permission over the setting model, but not the sensitive_email field
             self.user.is_superuser = False
             self.user.save()
    +        self.user.user_permissions.add(
    +            Permission.objects.get(
    +                content_type__app_label="wagtailadmin", codename="access_admin"
    +            ),
    +            Permission.objects.get(
    +                content_type__app_label="tests",
    +                codename="change_testpermissionedsitesetting",
    +            ),
    +        )
     
             self.assertFalse(TestPermissionedSiteSetting.objects.exists())
    +        # GET should provide a form with title but not sensitive_email
    +        response = self.get(setting=TestPermissionedSiteSetting)
    +        self.assertEqual(response.status_code, 200)
    +        self.assertIn("title", list(response.context["form"].fields))
    +        self.assertNotIn("sensitive_email", list(response.context["form"].fields))
    +
    +        # the GET creates a setting object, so remove any created objects prior to testing the POST
    +        TestPermissionedSiteSetting.objects.all().delete()
    +
    +        # POST should allow the title to be set, but not the sensitive_email
             response = self.post(
                 post_data={"sensitive_email": "test@example.com", "title": "test"},
                 setting=TestPermissionedSiteSetting,
    @@ -125,12 +170,30 @@ def test_create_restricted_field_without_permission(self):
             self.assertEqual(settings.sensitive_email, "")
     
         def test_create_restricted_field(self):
    +        # User has edit permission over the setting model, including the sensitive_email field
             self.user.is_superuser = False
             self.user.save()
             self.user.user_permissions.add(
    -            Permission.objects.get(codename="can_edit_sensitive_email_site_setting")
    +            Permission.objects.get(
    +                content_type__app_label="wagtailadmin", codename="access_admin"
    +            ),
    +            Permission.objects.get(
    +                content_type__app_label="tests",
    +                codename="change_testpermissionedsitesetting",
    +            ),
    +            Permission.objects.get(codename="can_edit_sensitive_email_site_setting"),
             )
             self.assertFalse(TestPermissionedSiteSetting.objects.exists())
    +        # GET should provide a form with title and sensitive_email
    +        response = self.get(setting=TestPermissionedSiteSetting)
    +        self.assertEqual(response.status_code, 200)
    +        self.assertIn("title", list(response.context["form"].fields))
    +        self.assertIn("sensitive_email", list(response.context["form"].fields))
    +
    +        # the GET creates a setting object, so remove any created objects prior to testing the POST
    +        TestPermissionedSiteSetting.objects.all().delete()
    +
    +        # POST should allow both title and sensitive_email to be set
             response = self.post(
                 post_data={"sensitive_email": "test@example.com", "title": "test"},
                 setting=TestPermissionedSiteSetting,
    @@ -153,11 +216,6 @@ def setUp(self):
             self.test_setting.save()
     
             self.user = self.login()
    -        self.user.user_permissions.add(
    -            Permission.objects.get(
    -                content_type__app_label="wagtailadmin", codename="access_admin"
    -            )
    -        )
     
         def test_get_edit(self):
             response = self.get()
    @@ -211,50 +269,117 @@ def test_get_redirect_to_relevant_instance_invalid(self):
             self.assertRedirects(response, status_code=302, expected_url="/admin/")
     
         def test_edit_restricted_field(self):
    +        # User has edit permission over the setting model, including the sensitive_email field
             test_setting = TestPermissionedSiteSetting()
    +        test_setting.title = "Old title"
             test_setting.sensitive_email = "test@example.com"
             test_setting.site = self.default_site
             test_setting.save()
             self.user.is_superuser = False
             self.user.save()
    -
             self.user.user_permissions.add(
    -            Permission.objects.get(codename="can_edit_sensitive_email_site_setting")
    +            Permission.objects.get(
    +                content_type__app_label="wagtailadmin", codename="access_admin"
    +            ),
    +            Permission.objects.get(
    +                content_type__app_label="tests",
    +                codename="change_testpermissionedsitesetting",
    +            ),
    +            Permission.objects.get(codename="can_edit_sensitive_email_site_setting"),
             )
     
    +        # GET should provide a form with title and sensitive_email
             response = self.get(setting=TestPermissionedSiteSetting)
             self.assertEqual(response.status_code, 200)
    +        self.assertIn("title", list(response.context["form"].fields))
             self.assertIn("sensitive_email", list(response.context["form"].fields))
     
    +        # POST should allow both title and sensitive_email to be set
             response = self.post(
                 setting=TestPermissionedSiteSetting,
    -            post_data={"sensitive_email": "test-updated@example.com", "title": "title"},
    +            post_data={
    +                "sensitive_email": "test-updated@example.com",
    +                "title": "New title",
    +            },
             )
             self.assertEqual(response.status_code, 302)
     
             test_setting.refresh_from_db()
             self.assertEqual(test_setting.sensitive_email, "test-updated@example.com")
    +        self.assertEqual(test_setting.title, "New title")
     
    -    def test_edit_restricted_field_without_permission(self):
    +    def test_edit_restricted_field_without_field_permission(self):
    +        # User has edit permission over the setting model, but not the sensitive_email field
             test_setting = TestPermissionedSiteSetting()
    +        test_setting.title = "Old title"
             test_setting.sensitive_email = "test@example.com"
             test_setting.site = self.default_site
             test_setting.save()
             self.user.is_superuser = False
             self.user.save()
    +        self.user.user_permissions.add(
    +            Permission.objects.get(
    +                content_type__app_label="wagtailadmin", codename="access_admin"
    +            ),
    +            Permission.objects.get(
    +                content_type__app_label="tests",
    +                codename="change_testpermissionedsitesetting",
    +            ),
    +        )
     
    +        # GET should provide a form with title but not sensitive_email
             response = self.get(setting=TestPermissionedSiteSetting)
             self.assertEqual(response.status_code, 200)
    +        self.assertIn("title", list(response.context["form"].fields))
             self.assertNotIn("sensitive_email", list(response.context["form"].fields))
     
    +        # POST should allow the title to be set, but not the sensitive_email
             response = self.post(
                 setting=TestPermissionedSiteSetting,
    -            post_data={"sensitive_email": "test-updated@example.com", "title": "title"},
    +            post_data={
    +                "sensitive_email": "test-updated@example.com",
    +                "title": "New title",
    +            },
             )
             self.assertEqual(response.status_code, 302)
     
             test_setting.refresh_from_db()
             self.assertEqual(test_setting.sensitive_email, "test@example.com")
    +        self.assertEqual(test_setting.title, "New title")
    +
    +    def test_edit_restricted_field_without_any_permission(self):
    +        # User has no permissions over the setting model, only access to the admin
    +        test_setting = TestPermissionedSiteSetting()
    +        test_setting.title = "Old title"
    +        test_setting.sensitive_email = "test@example.com"
    +        test_setting.site = self.default_site
    +        test_setting.save()
    +        self.user.is_superuser = False
    +        self.user.save()
    +        self.user.user_permissions.add(
    +            Permission.objects.get(
    +                content_type__app_label="wagtailadmin", codename="access_admin"
    +            ),
    +        )
    +
    +        # GET should redirect away with permission denied
    +        response = self.get(setting=TestPermissionedSiteSetting)
    +        self.assertRedirects(response, status_code=302, expected_url="/admin/")
    +
    +        # POST should redirect away with permission denied
    +        response = self.post(
    +            setting=TestPermissionedSiteSetting,
    +            post_data={
    +                "sensitive_email": "test-updated@example.com",
    +                "title": "New title",
    +            },
    +        )
    +        self.assertRedirects(response, status_code=302, expected_url="/admin/")
    +
    +        # The retrieved setting should be unchanged
    +        test_setting.refresh_from_db()
    +        self.assertEqual(test_setting.sensitive_email, "test@example.com")
    +        self.assertEqual(test_setting.title, "Old title")
     
     
     @override_settings(
    
  • wagtail/contrib/settings/views.py+3 0 modified
    @@ -14,6 +14,7 @@
     )
     from wagtail.admin.views import generic
     from wagtail.models import Site
    +from wagtail.permission_policies import ModelPermissionPolicy
     
     from .forms import SiteSwitchForm
     from .models import BaseGenericSetting, BaseSiteSetting
    @@ -81,11 +82,13 @@ def redirect_to_relevant_instance(request, app_name, model_name):
     class EditView(generic.EditView):
         template_name = "wagtailsettings/edit.html"
         error_message = gettext_lazy("The setting could not be saved due to errors.")
    +    permission_required = "change"
     
         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 = ModelPermissionPolicy(self.model)
             self.pk = kwargs.get(self.pk_url_kwarg)
             super().setup(request, app_name, model_name, *args, **kwargs)
     
    

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

4

News mentions

0

No linked articles in our index yet.