From 2c7e84dedd97bd71f5b836ad6207419c853d86e1 Mon Sep 17 00:00:00 2001 From: Vito Date: Sat, 31 Oct 2026 13:59:29 +0000 Subject: [PATCH] fix(security): send a generic LoRA scan error to the browser (#560) An unexpected exception's text could reach the page in the LoRA scan's error frame, including a path or a model name. Those failures now send a fixed message and keep the detail in the server log. Expected refusals keep their wording, and an OSError still shows its reason and the file's basename. Refs #552 --- py/api/lora_integration.py | 16 ++++++++++++++-- tests/test_api_lora.py | 17 +++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/py/api/lora_integration.py b/py/api/lora_integration.py index 86370f5b..55acc4f0 100644 --- a/py/api/lora_integration.py +++ b/py/api/lora_integration.py @@ -44,6 +44,7 @@ _FILES_CHANGED = ( "The LoRA metadata files changed during the scan; the previous imports " "were kept. Run the scan again." ) +_LORA_SCAN_FAILED = "The LoRA import failed. Check the server log for details." _IMPORTS_CHANGED = ( "The imported LoRA prompts changed during the scan; the previous imports " "were kept. Run the scan again." @@ -407,10 +408,21 @@ class LoraIntegrationMixin: return if isinstance(error, PlanChanged): message = _IMPORTS_CHANGED - elif isinstance(error, (LoraFilesChanged, LoraSettingsUnusable)): + elif isinstance( + error, + ( + LoraFilesChanged, + LoraSettingsUnusable, + EmptyReplacement, + PairDependencyError, + ), + ): message = str(error) - else: + elif isinstance(error, OSError): + # Reduced to its reason and the file's basename. message = self._public_error(error) + else: + message = _LORA_SCAN_FAILED if isinstance( error, ( diff --git a/tests/test_api_lora.py b/tests/test_api_lora.py index 99446360..9eb0f163 100644 --- a/tests/test_api_lora.py +++ b/tests/test_api_lora.py @@ -840,6 +840,23 @@ class TestLoraScanRoute(LoraAPITestCase): ("error", error["message"], 0), ) + async def test_unexpected_scan_error_is_generic_in_the_stream(self): + self._enable() + secret = str(self.tmp_root / "x.db") + err = RuntimeError(f"could not open {secret}") + with patch.object(self.api.db, "replace_category_prompts", side_effect=err): + with self.assertLogs("prompt_manager.api", level="ERROR") as logs: + resp = await self.client.request("POST", "/prompt_manager/lora/scan") + body = await resp.text() # the stream ends inside the patch + self.assertNotIn("x.db", body) + error = self._sse_events(body)[-1] + self.assertEqual( + (error["type"], error["status"], error["progress"]), + ("error", error["message"], 0), + ) + self.assertIn("server log", error["message"]) + self.assertIn(secret, "\n".join(logs.output)) + @staticmethod def _sse_events(text): return [