diff --git a/addons/wiki/models.py b/addons/wiki/models.py index e20f40dd730..f8c6a1bf75f 100644 --- a/addons/wiki/models.py +++ b/addons/wiki/models.py @@ -266,7 +266,16 @@ def get_for_node(self, node, name=None, id=None): deleted__isnull=True, node=node, ).order_by('created', 'id').first() - return WikiPage.load(id) + if id: + wiki_page = WikiPage.load(id) + # Restrict by node so wid cannot leak another project's page name. + if ( + wiki_page + and wiki_page.deleted is None + and wiki_page.node_id == node.id + ): + return wiki_page + return None def get_for_child_nodes(self, node, parent=None): if parent: diff --git a/addons/wiki/tests/test_wiki.py b/addons/wiki/tests/test_wiki.py index d6f267a6fb5..3be7918e3e3 100644 --- a/addons/wiki/tests/test_wiki.py +++ b/addons/wiki/tests/test_wiki.py @@ -448,6 +448,17 @@ def test_wiki_id_url_get_returns_404(self): res = self.app.get(url, expect_errors=True) assert_equal(res.status_code, 404) + def test_wiki_id_url_get_returns_404_for_other_node_wiki(self): + other = ProjectFactory(is_public=True, creator=self.user) + other_page = WikiPage.objects.create_for_node( + other, 'SecretFromOther', 'content', Auth(self.user) + ) + url = self.project.web_url_for( + 'project_wiki_id_page', wid=other_page._primary_key, _guid=True + ) + res = self.app.get(url, expect_errors=True) + assert_equal(res.status_code, 404) + def test_home_is_capitalized_in_web_view(self): url = self.project.web_url_for('project_wiki_home', wid='home', _guid=True) res = self.app.get(url, auth=self.user.auth).follow(auth=self.user.auth) @@ -3390,6 +3401,24 @@ def test_bulk_update_wiki_sort(self): self.assertEqual(result_wiki_child_page2, {'parent_id': wiki_page2_id, 'sort_order': 1}) self.assertEqual(result_wiki_child_page3, {'parent_id': wiki_child_page2_id, 'sort_order': 1}) + def test_bulk_update_wiki_sort_rejects_cross_node_parent(self): + other = ProjectFactory(is_public=True, creator=self.user) + other_page = WikiPage.objects.create_for_node( + other, 'other project wiki', 'content', self.consolidate_auth + ) + other_guid = other_page.guids.first()._id + parent_before = self.wiki_page1.parent_id + sort_id_list = [self.guid1] + sort_num_list = [1] + parent_wiki_id_list = [other_guid] + with self.assertRaises(HTTPError) as cm: + views._bulk_update_wiki_sort( + self.project, sort_id_list, sort_num_list, parent_wiki_id_list + ) + self.assertEqual(cm.exception.code, http_status.HTTP_400_BAD_REQUEST) + self.wiki_page1.reload() + self.assertEqual(self.wiki_page1.parent_id, parent_before) + def _wiki_sort_payload(self): return { 'sortedData': [ @@ -3431,6 +3460,39 @@ def test_project_update_wiki_page_sort(self): self.assertEqual(result_wiki_child_page2, {'parent_id': wiki_page2_id, 'sort_order': 2}) self.assertEqual(result_wiki_child_page3, {'parent_id': wiki_child_page2_id, 'sort_order': 1}) + def test_project_update_wiki_page_sort_rejects_cross_node_parent(self): + other = ProjectFactory(is_public=True, creator=self.user) + other_page = WikiPage.objects.create_for_node( + other, 'other project wiki', 'content', self.consolidate_auth + ) + other_guid = other_page.guids.first()._id + parent_before = self.wiki_page1.parent_id + url = self.project.api_url_for('project_update_wiki_page_sort') + # Nest a local wiki under another project's wiki id as parent. + payload = { + 'sortedData': [ + { + 'name': 'other as parent', + 'id': other_guid, + 'sortOrder': 1, + 'children': [ + { + 'name': 'wiki page1', + 'id': self.guid1, + 'sortOrder': 1, + 'children': [], + 'fold': False, + } + ], + 'fold': False, + } + ] + } + res = self.app.post_json(url, payload, auth=self.user.auth, expect_errors=True) + assert_equal(res.status_code, http_status.HTTP_400_BAD_REQUEST) + self.wiki_page1.reload() + self.assertEqual(self.wiki_page1.parent_id, parent_before) + @pytest.mark.enable_bookmark_creation class TestYWebsocketToken(OsfTestCase): diff --git a/addons/wiki/views.py b/addons/wiki/views.py index ef213addf82..bcf8b905095 100644 --- a/addons/wiki/views.py +++ b/addons/wiki/views.py @@ -93,6 +93,10 @@ message_short='Invalid request', message_long='The requested version of this wiki page does not exist.' )) +WIKI_INVALID_PARENT_ERROR = HTTPError(http_status.HTTP_400_BAD_REQUEST, data=dict( + message_short='Invalid parent wiki', + message_long='Parent wiki page does not exist on this project.', +)) WIKI_IMPORT_TASK_ALREADY_EXISTS = HTTPError(http_status.HTTP_400_BAD_REQUEST, data=dict( message_short='Running Task exists', @@ -1453,7 +1457,10 @@ def _bulk_update_wiki_sort(node, sort_id_list, sort_num_list, parent_wiki_id_lis parent_wiki_id = parent_wiki_id_list[idx] setattr(page, 'sort_order', sort_order_number) if parent_wiki_id is not None: - parent = WikiPage.objects.get(guids___id=parent_wiki_id) + # Same-node only; global guid lookup would allow cross-project parents. + parent = WikiPage.objects.get_for_node(node, id=parent_wiki_id) + if parent is None: + raise WIKI_INVALID_PARENT_ERROR setattr(page, 'parent', parent) else: setattr(page, 'parent', None)