diff --git a/py/api/images.py b/py/api/images.py index 7ded080d..45196f0f 100644 --- a/py/api/images.py +++ b/py/api/images.py @@ -3367,12 +3367,16 @@ class ImageRoutesMixin: status=429, ) try: - return await self._reveal(relative, root) + return await self._reveal(relative, root, linked_only=index >= len(outputs)) finally: gate.leave() - async def _reveal(self, relative: str, root: Path): - """Resolve and open, inside the reveal gate.""" + async def _reveal(self, relative: str, root: Path, *, linked_only=False): + """Resolve and open, inside the reveal gate. + + Under a LoRA-linked root the file that would open is checked again + (it may resolve through a path anchor): only a recorded one opens. + """ try: path = await self._run_in_executor(self._reveal_path, relative, root) except (OSError, ValueError) as exc: @@ -3383,6 +3387,10 @@ class ImageRoutesMixin: return _reveal_not_found() if path is None: return _reveal_not_found() + if linked_only and not await self._run_in_executor( + self._is_recorded_media, path + ): + return _forbidden() try: await asyncio.get_running_loop().run_in_executor( reveal_executor(), self._reveal_opener, path, root diff --git a/tests/test_image_reveal.py b/tests/test_image_reveal.py index e5b8e83e..50415862 100644 --- a/tests/test_image_reveal.py +++ b/tests/test_image_reveal.py @@ -715,6 +715,19 @@ class TestRevealRoute(ImageAPITestCase): ) self.assertEqual((status, data), (200, {"success": True})) self.assertEqual(self.opened, [Path(os.path.realpath(linked))]) + # The file that would open is checked too, whatever the request named + # (a path anchor could resolve it elsewhere in the root). + other = make_png(lora / "abc" / "other.png") + from py.api.images import ImageRoutesMixin + + with patch.object( + ImageRoutesMixin, "_resolve_client_media_path", return_value=other + ): + status, _data = await self.post( + {"root_index": 1, "root_scope": scope, "path": "abc/cover.png"} + ) + self.assertEqual(status, 403) + self.assertEqual(len(self.opened), 1) async def test_target_changed_before_launch_is_404(self): make_png(self.output_dir / "sub" / "a.png")