CVE-2024-35228 (Python, published 2024-05-30T19:15Z)
Description: value: 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`.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/wagtail/wagtail
Files changed:
wagtail/contrib/settings/tests/generic/test_admin.py
wagtail/contrib/settings/tests/site_specific/test_admin.py
wagtail/contrib/settings/views.py
Commit message: Restore permission check on settings EditView


Vulnerable code:
        self.user.user_permissions.add(
            Permission.objects.get(
                content_type__app_label="wagtailadmin", codename="access_admin"
            )
        )
    def test_create_restricted_field_without_permission(self):
            Permission.objects.get(codename="can_edit_sensitive_email_generic_setting")
        self.user.user_permissions.add(
            Permission.objects.get(
                content_type__app_label="wagtailadmin", codename="access_admin"
            )
        )
            Permission.objects.get(codename="can_edit_sensitive_email_generic_setting")
            post_data={"sensitive_email": "test-updated@example.com", "title": "title"},
    def test_edit_restricted_field_without_permission(self):
            post_data={"sensitive_email": "test-updated@example.com", "title": "title"},
        self.user.user_permissions.add(
            Permission.objects.get(
                content_type__app_label="wagtailadmin", codename="access_admin"
            )
        )
    def test_create_restricted_field_without_permission(self):
            Permission.objects.get(codename="can_edit_sensitive_email_site_setting")
        self.user.user_permissions.add(
            Permission.objects.get(
                content_type__app_label="wagtailadmin", codename="access_admin"
            )
        )

            Permission.objects.get(codename="can_edit_sensitive_email_site_setting")
            post_data={"sensitive_email": "test-updated@example.com", "title": "title"},
    def test_edit_restricted_field_without_permission(self):
            post_data={"sensitive_email": "test-updated@example.com", "title": "title"},

Fixed code:
    def test_create_restricted_field_without_any_permission(self):
        # User has no permissions over the setting model, only access to the admin
        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/")

        # 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
        # User has edit permission over the setting model, including the sensitive_email field
            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
        # User has edit permission over the setting model, including the sensitive_email field
        test_setting.title = "Old title"
            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
        self.assertIn("title", list(response.context["form"].fields))
        # POST should allow both title and sensitive_email to be set
            post_data={
                "sensitive_email": "test-updated@example.com",
                "title": "New title",
            },
        self.assertEqual(test_setting.title, "New title")
    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.title = "Old title"
        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
        self.assertIn("title", list(response.context["form"].fields))
        # POST should allow the title to be set, but not the sensitive_email
            post_data={
                "sensitive_email": "test-updated@example.com",
                "title": "New title",
            },
        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")
    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.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))

        # 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
        # User has edit permission over the setting model, including the sensitive_email field
            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))

        # 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
        # User has edit permission over the setting model, including the sensitive_email field
        test_setting.title = "Old title"
            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
        self.assertIn("title", list(response.context["form"].fields))
        # POST should allow both title and sensitive_email to be set
            post_data={
                "sensitive_email": "test-updated@example.com",
                "title": "New title",
            },
        self.assertEqual(test_setting.title, "New title")
    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.title = "Old title"
        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
        self.assertIn("title", list(response.context["form"].fields))
        # POST should allow the title to be set, but not the sensitive_email
            post_data={
                "sensitive_email": "test-updated@example.com",
                "title": "New title",
            },
        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")
from wagtail.permission_policies import ModelPermissionPolicy
    permission_required = "change"
        self.permission_policy = ModelPermissionPolicy(self.model)