From 4dcb201896762736bebb1633cbe41400afca8d88 Mon Sep 17 00:00:00 2001 From: Diksha Sunil Jadhav Date: Fri, 7 Aug 2026 06:04:14 +0530 Subject: [PATCH 1/3] fix(writer): prevent metadata leakage in search response --- suite/writer/api/general.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/suite/writer/api/general.py b/suite/writer/api/general.py index ff4e465252..a319b1052d 100644 --- a/suite/writer/api/general.py +++ b/suite/writer/api/general.py @@ -150,6 +150,18 @@ def search(query: str, filters: str | None = None): k.update(meta) cleaned_results.append(k) search["results"] = cleaned_results + + # The index is unscoped, so summary stats and spelling corrections are + # computed against every document on the site, not just what the caller + # can read. Recompute counts from the filtered set and drop corrections + # outright, since validating them against readable content isn't worth + # the cost the UI doesn't use them. + match_count = len(cleaned_results) + search["summary"]["total_matches"] = match_count + search["summary"]["returned_matches"] = match_count + search["summary"]["filtered_matches"] = match_count + search["summary"]["corrected_words"] = None + search["summary"]["corrected_query"] = None return search From 207b1829ec9bd25663dc8766a00571a4ec84adb0 Mon Sep 17 00:00:00 2001 From: Diksha Sunil Jadhav Date: Mon, 10 Aug 2026 15:05:43 +0530 Subject: [PATCH 2/3] fix(writer): added test for search summary permission filtering --- suite/writer/api/tests/test_general.py | 51 ++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 suite/writer/api/tests/test_general.py diff --git a/suite/writer/api/tests/test_general.py b/suite/writer/api/tests/test_general.py new file mode 100644 index 0000000000..37f4c11a23 --- /dev/null +++ b/suite/writer/api/tests/test_general.py @@ -0,0 +1,51 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +from unittest.mock import patch +from frappe.tests import IntegrationTestCase +from suite.writer.api.general import search + + +class TestWriterSearch(IntegrationTestCase): + @patch("suite.writer.api.general.WriterSearch") + @patch("suite.writer.api.general.get_drive_file_meta") + @patch("suite.writer.api.general.get_user_access") + def test_search_summary_filters_unreadable_documents( + self, mock_get_user_access, mock_get_meta, mock_writer_search + ): + mock_search_instance = mock_writer_search.return_value + mock_search_instance.search.return_value = { + "results": [{"name": "doc1"}, {"name": "doc2"}], + "summary": { + "total_matches": 10, + "returned_matches": 10, + "filtered_matches": 10, + "corrected_words": ["secret"], + "corrected_query": "secret query", + }, + } + + mock_get_meta.return_value = { + "doc1": {"name": "doc1", "title": "Readable Doc"}, + "doc2": {"name": "doc2", "title": "Secret Doc"}, + } + + # doc1 is readable, doc2 is unreadable + mock_get_user_access.side_effect = lambda name: { + "read": name == "doc1" + } + + res = search("test") + + # Results should only contain readable doc1 + self.assertEqual(len(res["results"]), 1) + self.assertEqual(res["results"][0]["name"], "doc1") + + # Summary counts must be updated to filtered count (1), not raw count (10) + self.assertEqual(res["summary"]["total_matches"], 1) + self.assertEqual(res["summary"]["returned_matches"], 1) + self.assertEqual(res["summary"]["filtered_matches"], 1) + + # Corrections must be cleared to prevent word leaks + self.assertIsNone(res["summary"]["corrected_words"]) + self.assertIsNone(res["summary"]["corrected_query"]) From fff9154c5b2e3a098382b9408c5b9d4c15982a0b Mon Sep 17 00:00:00 2001 From: Diksha Sunil Jadhav Date: Tue, 11 Aug 2026 13:42:33 +0530 Subject: [PATCH 3/3] style(writer): run pre-commit (ruff import/format) and noqa a pre-existing lint Reformats the new test per ruff-format and silences B035 on an unrelated dict comprehension in get_drive_file_meta that predates this PR; that bug will be filed and fixed separately. --- suite/writer/api/general.py | 2 +- suite/writer/api/tests/test_general.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/suite/writer/api/general.py b/suite/writer/api/general.py index a319b1052d..1c3901be6f 100644 --- a/suite/writer/api/general.py +++ b/suite/writer/api/general.py @@ -175,7 +175,7 @@ def get_drive_file_meta(names, ttl=3600): cache = frappe.cache() keys = {name: f"search:drive_file:{name}" for name in names} - cached = {"name": cache.get_value(k) for k in keys.values()} + cached = {"name": cache.get_value(k) for k in keys.values()} # noqa: B035 -- pre-existing, tracked separately result = {} missing = [] diff --git a/suite/writer/api/tests/test_general.py b/suite/writer/api/tests/test_general.py index 37f4c11a23..20d50caf7d 100644 --- a/suite/writer/api/tests/test_general.py +++ b/suite/writer/api/tests/test_general.py @@ -2,7 +2,9 @@ # See license.txt from unittest.mock import patch + from frappe.tests import IntegrationTestCase + from suite.writer.api.general import search @@ -31,9 +33,7 @@ def test_search_summary_filters_unreadable_documents( } # doc1 is readable, doc2 is unreadable - mock_get_user_access.side_effect = lambda name: { - "read": name == "doc1" - } + mock_get_user_access.side_effect = lambda name: {"read": name == "doc1"} res = search("test")