from html import unescape from django.contrib.auth import get_user_model from django.contrib.sites.models import Site from django.core.cache import cache from django.core.exceptions import ValidationError from django.test import override_settings from django.utils.translation import override as force_language from cms.admin import forms from cms.admin.forms import ( AddPageForm, DuplicatePageForm, GlobalPagePermissionAdminForm, PagePermissionInlineAdminForm, PageUserGroupForm, ViewRestrictionInlineAdminForm, get_permission_accessor, ) from cms.api import assign_user_to_page, create_page, create_page_content from cms.forms.fields import PageSelectFormField from cms.forms.utils import ( get_page_choices, get_site_choices, update_site_and_page_choices, ) from cms.forms.widgets import ApplicationConfigSelect from cms.models import ACCESS_PAGE, ACCESS_PAGE_AND_CHILDREN, Page, PageContent from cms.test_utils.testcases import ( URL_CMS_PAGE_ADVANCED_CHANGE, URL_CMS_PAGE_PERMISSIONS, CMSTestCase, ) from cms.utils.permissions import current_user class Mock_PageSelectFormField(PageSelectFormField): def __init__(self, required=False): # That's to have a proper mock object, without having to resort # to dirtier tricks. We want to test *just* compress here. self.required = required self.error_messages = {} self.error_messages["invalid_page"] = "Invalid_page" class FormsTestCase(CMSTestCase): def setUp(self): cache.clear() self.site = Site.objects.get_current() def test_get_site_choices(self): result = get_site_choices() self.assertEqual(result, []) def test_get_page_choices(self): result = get_page_choices() self.assertEqual(result, [("", "----")]) def test_page_choices_draft_only(self): """ The page choices should always use draft ids """ pages = [ create_page("0001", "nav_playground.html", "en"), create_page("0002", "nav_playground.html", "en"), create_page("0003", "nav_playground.html", "en"), create_page("0004", "nav_playground.html", "en"), ] expected = [("", "----"), (self.site.name, [(page.pk, page.get_title("en", fallback=False)) for page in pages])] self.assertSequenceEqual(get_page_choices("en"), expected) def test_get_page_choices_with_multiple_translations(self): pages = [ create_page("0001", "nav_playground.html", "en"), create_page("0002", "nav_playground.html", "en"), create_page("0003", "nav_playground.html", "en"), create_page("0004", "nav_playground.html", "en"), ] languages = ["de", "fr"] for page in pages: for language in languages: title = page.get_title("en") create_page_content(language, title, page=page) for language in ["en"] + languages: expected = [ ("", "----"), (self.site.name, [(page.pk, page.get_title(language, fallback=False)) for page in pages]), ] with force_language(language): self.assertSequenceEqual(get_page_choices(), expected) def test_get_site_choices_without_moderator(self): result = get_site_choices() self.assertEqual(result, []) def test_get_site_choices_without_moderator_with_superuser(self): # boilerplate (creating a page) User = get_user_model() fields = dict(is_staff=True, is_active=True, is_superuser=True, email="super@super.com") if User.USERNAME_FIELD != "email": fields[User.USERNAME_FIELD] = "super" user_super = User(**fields) user_super.set_password(getattr(user_super, User.USERNAME_FIELD)) user_super.save() with self.login_user_context(user_super): create_page("home", "nav_playground.html", "en", created_by=user_super) # The proper test result = get_site_choices() self.assertEqual(result, [(1, "example.com")]) def test_compress_function_raises_when_page_is_none(self): raised = False try: fake_field = Mock_PageSelectFormField(required=True) data_list = (0, None) # (site_id, page_id) dsite-id is not used fake_field.compress(data_list) self.fail("compress function didn't raise!") except forms.ValidationError: raised = True self.assertTrue(raised) def test_compress_function_returns_none_when_not_required(self): fake_field = Mock_PageSelectFormField(required=False) data_list = (0, None) # (site_id, page_id) dsite-id is not used result = fake_field.compress(data_list) self.assertEqual(result, None) def test_compress_function_returns_none_when_no_data_list(self): fake_field = Mock_PageSelectFormField(required=False) data_list = None result = fake_field.compress(data_list) self.assertEqual(result, None) def test_compress_function_gets_a_page_when_one_exists(self): # boilerplate (creating a page) User = get_user_model() fields = dict(is_staff=True, is_active=True, is_superuser=True, email="super@super.com") if User.USERNAME_FIELD != "email": fields[User.USERNAME_FIELD] = "super" user_super = User(**fields) user_super.set_password(getattr(user_super, User.USERNAME_FIELD)) user_super.save() with self.login_user_context(user_super): home_page = create_page("home", "nav_playground.html", "en", created_by=user_super) # The actual test fake_field = Mock_PageSelectFormField() data_list = (0, home_page.pk) # (site_id, page_id) dsite-id is not used result = fake_field.compress(data_list) self.assertEqual(home_page, result) def test_update_site_and_page_choices(self): Site.objects.all().delete() site = Site.objects.create(domain="http://www.django-cms.org", name="Django CMS", pk=1) page1 = create_page("Page 1", "nav_playground.html", "en", site=site) page2 = create_page("Page 2", "nav_playground.html", "de", site=site) page3 = create_page("Page 3", "nav_playground.html", "en", site=site, parent=page1) # Check for injection attacks page4 = create_page( 'Page 4', "nav_playground.html", "en", site=site, parent=page1 ) # enforce the choices to be casted to a list site_choices, page_choices = (list(bit) for bit in update_site_and_page_choices("en")) self.assertEqual( page_choices, [ ("", "----"), ( site.name, [ (page1.pk, "Page 1"), (page3.pk, "  Page 3"), (page4.pk, "  Page 4<script>alert("bad-things");</script>"), (page2.pk, "Page 2"), ], ), ], ) self.assertEqual(site_choices, [(site.pk, site.name)]) def test_app_config_select_escaping(self): class FakeAppConfig: def __init__(self, pk, config): self.pk = pk self.config = config def __str__(self): return self.config class FakeApp: def __init__(self, name, configs=()): self.name = name self.configs = configs def __str__(self): return self.name def get_configs(self): return self.configs def get_config_add_url(self): return "/fake/url/" GoodApp = FakeApp( "GoodApp", [ FakeAppConfig(1, "good-app-one-config"), FakeAppConfig(2, "good-app-two-config"), ], ) BadApp = FakeApp( "BadApp", [ FakeAppConfig(1, "bad-app-one-config"), FakeAppConfig(2, 'bad-app-two-config'), ], ) app_configs = { GoodApp: GoodApp, BadApp: BadApp, } app_config_select = ApplicationConfigSelect(app_configs=app_configs) output = app_config_select.render("application_configurations", 1) self.assertFalse('' in output) self.assertTrue('\\u003Cscript\\u003Ealert(\\"bad-stuff\\");\\u003C/script\\u003E' in output) def test_move_page_form(self): """Test the MovePageForm validation and behavior""" # Create a basic page structure parent1 = create_page("Parent 1", "nav_playground.html", "en") parent2 = create_page("Parent 2", "nav_playground.html", "en") child1 = create_page("Child 1", "nav_playground.html", "en", parent=parent1) # Test valid move between parents data = { "target": parent2.pk, "position": "0", # first-child "site": self.site.pk, } form = forms.MovePageForm(data=data, page=child1, site=self.site) self.assertTrue(form.is_valid(), form.errors) # Test moving page with same slug under different parents child2 = create_page("Child 2", "nav_playground.html", "en", parent=parent2) data = { "target": parent1.pk, "position": "0", # first-child "site": self.site.pk, } form = forms.MovePageForm(data=data, page=child2, site=self.site) self.assertTrue(form.is_valid(), form.errors) # Test moving page to a position that would create duplicate slug child3 = create_page("Child 1", "nav_playground.html", "en", parent=parent2) # Same slug as child1 data = { "target": parent1.pk, "position": "0", # first-child "site": self.site.pk, } form = forms.MovePageForm(data=data, page=child3, site=self.site) self.assertFalse(form.is_valid()) self.assertIn("You cannot have two pages with the same slug", str(form.errors["__all__"])) # Test moving homepage homepage = create_page("Home", "nav_playground.html", "en") homepage.set_as_homepage() data = { "target": parent1.pk, "position": "0", # first-child "site": self.site.pk, } form = forms.MovePageForm(data=data, page=homepage, site=self.site) self.assertFalse(form.is_valid()) self.assertIn("You can't move the home page inside another page", str(form.errors["target"])) def test_move_page_form_without_urls(self): target = create_page("Target", "nav_playground.html", "en") page = create_page("Unpublished", "nav_playground.html", "en") # A never-published page can have content but no public URL yet. page.urls.all().delete() for destination in ("", target.pk): with self.subTest(target=destination): form = forms.MovePageForm( data={"target": destination, "position": 0}, page=page, site=self.site ) self.assertTrue(form.is_valid(), form.errors) form.move_page() page.refresh_from_db() self.assertEqual(page.parent_id, destination or None) self.assertFalse(page.urls.exists()) def test_move_page_form_checks_all_url_languages(self): source = create_page("Source", "nav_playground.html", "en") target = create_page("Target", "nav_playground.html", "en") create_page_content("de", "Ziel", target, slug="ziel") page = create_page("Child", "nav_playground.html", "en", parent=source) create_page_content("de", "Kind", page, slug="kind") create_page("Kind", "nav_playground.html", "de", parent=target, slug="kind") with force_language("en"): form = forms.MovePageForm( data={"target": target.pk, "position": 0}, page=page, site=self.site ) self.assertFalse(form.is_valid()) self.assertIn("You cannot have two pages with the same slug", str(form.errors["__all__"])) def test_move_page_form_keeps_overwritten_urls(self): source = create_page("Source", "nav_playground.html", "en") target = create_page("Target", "nav_playground.html", "en") create_page_content("de", "Ziel", target, slug="ziel") page = create_page("Child", "nav_playground.html", "en", parent=source) create_page_content("de", "Kind", page, slug="kind", overwrite_url="custom") # Would collide with the synthesized path "ziel/kind", but the # overwritten German URL stays at "custom" after the move. create_page("Kind", "nav_playground.html", "de", parent=target, slug="kind") with force_language("en"): form = forms.MovePageForm( data={"target": target.pk, "position": 0}, page=page, site=self.site ) self.assertTrue(form.is_valid(), form.errors) form.move_page() page.refresh_from_db() self.assertEqual(page.parent_id, target.pk) self.assertEqual(page.get_path("de"), "custom") def test_move_page_form_keeps_overwritten_urls_next_to_sibling(self): source = create_page("Source", "nav_playground.html", "en") target = create_page("Target", "nav_playground.html", "en") create_page_content("de", "Ziel", target, slug="ziel") page = create_page("Child", "nav_playground.html", "en", parent=source) create_page_content("de", "Kind", page, slug="kind", overwrite_url="custom") create_page("Other", "nav_playground.html", "en", parent=target) # A sibling with the same German slug does not collide with the # overwritten German URL, whatever the insert position. create_page("Kind", "nav_playground.html", "de", parent=target, slug="kind") with force_language("en"): form = forms.MovePageForm( data={"target": target.pk, "position": 1}, page=page, site=self.site ) self.assertTrue(form.is_valid(), form.errors) form.move_page() page.refresh_from_db() self.assertEqual(page.parent_id, target.pk) self.assertEqual(page.get_path("de"), "custom") def test_move_page_form_reorders_home_page(self): home = create_page("Home", "nav_playground.html", "en") create_page_content("de", "Start", home, slug="start") home.set_as_homepage() other = create_page("Other", "nav_playground.html", "en") # Matches the German slug of the home page, whose URLs never change # when it is reordered among the root pages. create_page_content("de", "Andere", other, slug="andere", overwrite_url="start") with force_language("en"): form = forms.MovePageForm(data={"target": "", "position": 1}, page=home, site=self.site) self.assertTrue(form.is_valid(), form.errors) form.move_page() home.refresh_from_db() self.assertTrue(home.is_home) self.assertEqual(home.get_path("de"), "") def test_move_page_form_below_unreachable_parent(self): target = create_page("Target", "nav_playground.html", "en") create_page_content("de", "Ziel", target, slug="ziel") # The German translation of the target has no public path. target.urls.filter(language="de").update(path=None) source = create_page("Source", "nav_playground.html", "en") create_page_content("de", "Quelle", source, slug="quelle") page = create_page("Child", "nav_playground.html", "en", parent=source) create_page_content("de", "Kind", page, slug="kind") # Would collide if the page were checked as a root page, but below an # unreachable parent its German URL stays unreachable. create_page("Kind", "nav_playground.html", "de", slug="kind") with force_language("en"): form = forms.MovePageForm( data={"target": target.pk, "position": 0}, page=page, site=self.site ) self.assertTrue(form.is_valid(), form.errors) form.move_page() page.refresh_from_db() self.assertEqual(page.parent_id, target.pk) self.assertIsNone(page.get_path("de")) def test_move_page_form_positions(self): """Test different position options in MovePageForm""" parent = create_page("Parent", "nav_playground.html", "en") child1 = create_page("Child 1", "nav_playground.html", "en", parent=parent) child2 = create_page("Child 2", "nav_playground.html", "en", parent=parent) positions = ( ("0", "first-child"), # First child of target ("1", "last-child"), # Last child of target ("2", "left"), # Left sibling of target ("3", "right"), # Right sibling of target ) for position_id, position_name in positions: data = { "target": child2.pk, "position": position_id, # Use the numeric position ID "site": self.site.pk, } form = forms.MovePageForm(data=data, page=child1, site=self.site) self.assertTrue( form.is_valid(), f"Form should be valid for position {position_name}, errors: {form.errors}" ) def test_move_page_form_sibling_slug_collision(self): """Test that pages cannot be moved to create duplicate slugs at the same level""" # Create two separate parent pages parent1 = create_page("Parent 1", "nav_playground.html", "en", slug="parent-1") parent2 = create_page("Parent 2", "nav_playground.html", "en", slug="parent-2") # Create a child under parent1 with slug "test" create_page("Test Page", "nav_playground.html", "en", slug="test", parent=parent1) # Create a child under parent2 with the same slug "test" (this is allowed because they have different parents) child2 = create_page("Test Page", "nav_playground.html", "en", slug="test", parent=parent2) # Try to move child2 to be under parent1 (should fail because child1 is already there with same slug) data = { "target": parent1.pk, "position": 2, # 2 = first-child position "site": self.site.pk, } form = forms.MovePageForm(data=data, page=child2, site=self.site) is_valid = form.is_valid() self.assertFalse(is_valid, "Form should be invalid due to slug collision") self.assertIn( "You cannot have two pages with the same slug", str(form.errors["__all__"]), "Form should report slug collision error", ) def test_move_root_page_form_ignores_other_sites(self): """Moving a root page must only check its siblings on the page's own site (#8830)""" site2 = Site.objects.create(pk=2, domain="site2.com", name="site2") # Identical root level trees on both sites, as produced by ``manage.py cms copy-site`` for site in (self.site, site2): create_page("A", "nav_playground.html", "de", slug="a", site=site) create_page("B", "nav_playground.html", "de", slug="b", site=site) page_b = Page.objects.filter(site=site2, urls__slug="b", urls__language="de").distinct().get() data = { "target": "", # root level "position": 0, "site": site2.pk, } with force_language("de"): form = forms.MovePageForm(data=data, page=page_b, site=site2) self.assertTrue(form.is_valid(), form.errors) def test_move_page_form_child_parent_slug_collision(self): """Test that pages cannot be moved to create duplicate slugs at the same level""" # Create grandparent page grandparent = create_page("Grandparent", "nav_playground.html", "en", slug="grandparent") # Create parent page parent = create_page("Parent", "nav_playground.html", "en", slug="parent", parent=grandparent) # Create a child under parent with the same slug child = create_page("Child", "nav_playground.html", "en", slug="parent", parent=parent) # Try to move child to be on the same level as parent (should fail because parent is already there with same slug) data = { "target": grandparent.pk, "position": 2, # 2 = first-child position "site": self.site.pk, } form = forms.MovePageForm(data=data, page=child, site=self.site) is_valid = form.is_valid() self.assertFalse(is_valid, "Form should be invalid due to slug collision") self.assertIn( "You cannot have two pages with the same slug", str(form.errors["__all__"]), "Form should report slug collision error", ) class PermissionFormTestCase(CMSTestCase): def test_permission_forms(self): page = create_page("page_b", "nav_playground.html", "en", created_by=self.get_superuser()) normal_user = self._create_user("randomuser", is_staff=True, add_default_permissions=True) assign_user_to_page(page, normal_user, can_view=True, can_change=True) with self.login_user_context(self.get_superuser()): response = self.client.get(URL_CMS_PAGE_ADVANCED_CHANGE % page.pk) self.assertEqual(response.status_code, 200) response = self.client.get(URL_CMS_PAGE_PERMISSIONS % page.pk) self.assertEqual(response.status_code, 200) with self.settings(CMS_RAW_ID_USERS=True), current_user(self.get_superuser()): data = { "page": page.pk, "grant_on": "hello", "user": normal_user.pk, } form = PagePermissionInlineAdminForm(data=data, files=None) self.assertFalse(form.is_valid()) data = { "page": page.pk, "grant_on": ACCESS_PAGE, "user": normal_user.pk, } form = PagePermissionInlineAdminForm(data=data, files=None) self.assertTrue(form.is_valid()) form.save() data = { "page": page.pk, "grant_on": ACCESS_PAGE_AND_CHILDREN, "can_add": "1", "can_change": "", "user": normal_user.pk, } form = PagePermissionInlineAdminForm(data=data, files=None) error_message = ( "
  • Users can't create a page without also being able to " "change it. Please also enable 'Can edit'.
  • " ) self.assertFalse(form.is_valid()) self.assertTrue(error_message in unescape(str(form.errors))) data = { "page": page.pk, "grant_on": ACCESS_PAGE, "can_add": "1", "can_change": "1", "user": normal_user.pk, } form = PagePermissionInlineAdminForm(data=data, files=None) self.assertFalse(form.is_valid()) self.assertTrue( "
  • Add page permission requires also access to children, or " "descendants, otherwise added page can't be changed by its " "creator.
  • " in unescape(str(form.errors)) ) def test_inlines(self): user = self._create_user("randomuser", is_staff=True, add_default_permissions=True) current_user = self.get_superuser() page = create_page("page_b", "nav_playground.html", "en", created_by=current_user) data = { "page": page.pk, "grant_on": ACCESS_PAGE_AND_CHILDREN, "can_view": "True", "user": user.pk, "group": "", } form = ViewRestrictionInlineAdminForm(data=data, files=None) self.assertTrue(form.is_valid()) data = {"page": page.pk, "grant_on": ACCESS_PAGE_AND_CHILDREN, "can_view": "True", "group": ""} form = GlobalPagePermissionAdminForm(data=data, files=None) self.assertFalse(form.is_valid()) data = { "page": page.pk, "grant_on": ACCESS_PAGE_AND_CHILDREN, "can_view": "True", "user": user.pk, } form = GlobalPagePermissionAdminForm(data=data, files=None) self.assertTrue(form.is_valid()) def test_user_forms(self): user = self.get_superuser() data = {"name": "test_group"} form = PageUserGroupForm(data=data, files=None) form._current_user = user self.assertTrue(form.is_valid(), form.errors) instance = form.save() form = PageUserGroupForm(data=data, files=None, instance=instance) form._current_user = user self.assertTrue(form.is_valid(), form.errors) form.save() def test_page_user_group_form_drops_unauthorized_permissions(self): """A delegated manager must not be able to grant a group permissions they do not hold themselves, even by POSTing the hidden ``can_*`` fields that ``get_fieldsets`` never rendered for them (CWE-269). """ # Manager holds change_page but NOT any pagepermission/pageuser perms. manager = self._create_user("manager", is_staff=True, permissions=["change_page"]) self.assertTrue(manager.has_perm("cms.change_page")) self.assertFalse(manager.has_perm("cms.change_pagepermission")) data = { "name": "evil_group", "can_change_page": True, # held by the manager -> must be kept "can_change_pagepermission": True, # NOT held -> must be dropped "can_add_pagepermission": True, # NOT held -> must be dropped "can_change_pageuser": True, # NOT held -> must be dropped } form = PageUserGroupForm(data=data, files=None) form._current_user = manager self.assertTrue(form.is_valid(), form.errors) group = form.save() granted = set(get_permission_accessor(group).values_list("codename", flat=True)) self.assertIn("change_page", granted) self.assertNotIn("change_pagepermission", granted) self.assertNotIn("add_pagepermission", granted) self.assertNotIn("change_pageuser", granted) def test_page_user_group_form_superuser_can_grant_anything(self): """A superuser holds every permission, so nothing is dropped.""" superuser = self.get_superuser() data = { "name": "trusted_group", "can_change_page": True, "can_change_pagepermission": True, } form = PageUserGroupForm(data=data, files=None) form._current_user = superuser self.assertTrue(form.is_valid(), form.errors) group = form.save() granted = set(get_permission_accessor(group).values_list("codename", flat=True)) self.assertIn("change_page", granted) self.assertIn("change_pagepermission", granted) class DuplicatePageFormSecurityTestCase(CMSTestCase): def _build_form(self, user, source): request = self.get_request("/en/") request.user = user # The admin sets ``_site``/``_request`` as class attributes via # ``get_form`` before instantiation (they are read in ``__init__``). form_cls = type( "TestDuplicatePageForm", (DuplicatePageForm,), {"_site": Site.objects.get_current(), "_request": request}, ) return form_cls(data={"source": source.pk, "title": "x", "slug": "x"}) def test_duplicate_rejects_source_user_cannot_view(self): """A staff user with only 'add page' rights must not be able to copy a page they are not allowed to view (CWE-639 / CWE-862). ``source`` is a hidden, client-controlled field whose queryset spans every page. """ superuser = self.get_superuser() secret = create_page("secret", "nav_playground.html", "en", created_by=superuser) # Add a view restriction so the page is only viewable by the superuser. self.add_page_permission(superuser, secret, can_view=True) attacker = self._create_user("attacker", is_staff=True, add_default_permissions=True) with self.settings(CMS_PERMISSION=True): form = self._build_form(attacker, secret) self.assertFalse(form.is_valid()) self.assertIn("source", form.errors) def test_duplicate_allows_source_user_can_view(self): """Copying a page the user is allowed to view keeps working.""" superuser = self.get_superuser() source = create_page("source", "nav_playground.html", "en", created_by=superuser) with self.settings(CMS_PERMISSION=True): form = self._build_form(superuser, source) # ``source`` must not be the reason the form is (in)valid. form.is_valid() self.assertNotIn("source", form.errors) class AddPageFormSecurityTestCase(CMSTestCase): def _build_form(self, user, page, site): request = self.get_request("/en/") request.user = user form_cls = type( "TestAddPageForm", (AddPageForm,), # Admin form construction can omit ``source``, causing # AddPageForm.__init__ to return before narrowing cms_page. { "source": None, "_site": site, "_request": request, "Meta": type("Meta", (AddPageForm.Meta,), {"fields": []}), }, ) return form_cls(data={"cms_page": page.pk, "title": "French", "slug": "french"}) def test_rejects_cms_page_from_another_site(self): owner = self.get_superuser() site = Site.objects.get_current() other_site = Site.objects.create(domain="other.example.com", name="other.example.com") allowed_page = create_page("allowed", "nav_playground.html", "en", site=site, created_by=owner) page = create_page("secret", "nav_playground.html", "de", site=other_site, created_by=owner) attacker = self._create_user("attacker", is_staff=True, add_default_permissions=True) assign_user_to_page(allowed_page, attacker, can_change=True) with self.settings(CMS_PERMISSION=True): form = self._build_form(attacker, page, site) self.assertFalse(form.is_valid()) self.assertIn("cms_page", form.errors) def test_rejects_cms_page_user_cannot_change(self): owner = self.get_superuser() site = Site.objects.get_current() allowed_page = create_page("allowed", "nav_playground.html", "en", site=site, created_by=owner) page = create_page("secret", "nav_playground.html", "en", site=site, created_by=owner) attacker = self._create_user("attacker", is_staff=True, add_default_permissions=True) assign_user_to_page(allowed_page, attacker, can_change=True) with self.settings(CMS_PERMISSION=True): form = self._build_form(attacker, page, site) self.assertFalse(form.is_valid()) self.assertIn("cms_page", form.errors) class ValidateUrlTestCase(CMSTestCase): """Regression tests for the dangerous-scheme bypass in ``validate_url``. ``relative_url_regex`` matches any value whose first segment contains no ``/<>``; a scheme such as ``javascript:`` was therefore accepted as a "relative URL" and never reached ``URLValidator``. Such schemes execute script when the value is later rendered into an ``href`` (CWE-79). """ # Script-executing schemes -- these are the actual XSS vector. DANGEROUS = [ "javascript:alert(1)/x", "JavaScript:alert(document.domain)/a", "data:text/html,", "vbscript:msgbox(1)/x", ] # Markup anywhere in the path -- ``relative_url_regex`` used to constrain only the # first character after the first "/", so prefixing the markup with a safe character # was enough to slip it through. MARKUP = [ "foo/a", "/a", "foo/bar/", "/en/x'\">", # A value carrying a scheme takes the ``URLValidator`` branch instead, whose path # pattern accepts "<" and ">" as long as the value has no space. Markup must be # rejected there too. "https://example.com/", "https://example.com/", "https://example.com/a`` skipped validation altogether and was stored verbatim. """ def test_markup_is_rejected(self): from cms.forms.validators import validate_path_segment for value in ("", "ab"): with self.subTest(value=value): with self.assertRaises(ValidationError): validate_path_segment(value) def test_plain_segments_are_accepted(self): from cms.forms.validators import validate_path_segment # "" is the home page's path for value in ("", "contact", "contact-us", "Kontakt", "a.b_c"): with self.subTest(value=value): validate_path_segment(value) # must not raise def test_validate_url_uniqueness_rejects_markup(self): from cms.forms.validators import validate_url_uniqueness page = create_page("home", "nav_playground.html", "en") with self.assertRaises(ValidationError): validate_url_uniqueness(page.site, path="", language="en", exclude_page=page) @override_settings(CMS_PERMISSION=False) class OverwriteUrlFormValidationTestCase(CMSTestCase): """``ChangePageForm`` must reject markup in "Overwrite URL". ``clean()`` returns early for the home page, so the check cannot live there: the value is persisted regardless and becomes the live ``PageUrl.path`` as soon as the page stops being the home page. """ payload = "" def _post_overwrite_url(self, content, user, payload=None): data = { "title": content.title, "slug": content.slug, "overwrite_url": self.payload if payload is None else payload, "template": "nav_playground.html", "_continue": "1", } with self.login_user_context(user): return self.client.post(self.get_admin_url(PageContent, "change", content.pk), data) def _editor(self): editor = self.get_staff_user_with_no_permissions() self.add_permission(editor, "change_page") return editor def test_rejected_on_regular_page(self): home = create_page("home", "nav_playground.html", "en") home.set_as_homepage() page = create_page("victim", "nav_playground.html", "en", parent=home) content = page.get_content_obj("en") response = self._post_overwrite_url(content, self._editor()) self.assertEqual(response.status_code, 200) # re-rendered with errors, not a 302 redirect self.assertIn("overwrite_url", response.context["adminform"].form.errors) content.refresh_from_db() self.assertIsNone(content.overwrite_url) def test_rejected_in_later_path_segment(self): """Markup preceded by a safe character in a multi-segment path must not slip through.""" home = create_page("home", "nav_playground.html", "en") home.set_as_homepage() page = create_page("victim", "nav_playground.html", "en", parent=home) content = page.get_content_obj("en") response = self._post_overwrite_url(content, self._editor(), payload="foo/a") self.assertEqual(response.status_code, 200) self.assertIn("overwrite_url", response.context["adminform"].form.errors) content.refresh_from_db() self.assertIsNone(content.overwrite_url) def test_rejected_on_home_page(self): home = create_page("home", "nav_playground.html", "en") home.set_as_homepage() other = create_page("other", "nav_playground.html", "en") content = home.get_content_obj("en") response = self._post_overwrite_url(content, self._editor()) self.assertEqual(response.status_code, 200) self.assertIn("overwrite_url", response.context["adminform"].form.errors) content.refresh_from_db() self.assertIsNone(content.overwrite_url) # the value must not resurface once the page is no longer the home page other.set_as_homepage() home = Page.objects.get(pk=home.pk) home._clear_internal_cache() home.update_urls_from_content("en") home._clear_internal_cache() self.assertEqual(home.urls.get(language="en").path, "home")