diff --git a/haystack/components/converters/image/image_utils.py b/haystack/components/converters/image/image_utils.py index 1775ae2aa53..742b08a9ce5 100644 --- a/haystack/components/converters/image/image_utils.py +++ b/haystack/components/converters/image/image_utils.py @@ -248,6 +248,18 @@ def _extract_image_sources_info( ) resolved_file_path = Path(root_path, file_path) + + # When root_path is set, ensure the resolved path stays within it to block path-traversal + # payloads (e.g. "../../etc/passwd") coming from document metadata. + if root_path: + resolved_file_path = resolved_file_path.resolve() + resolved_root = Path(root_path).resolve() + if not resolved_file_path.is_relative_to(resolved_root): + raise ValueError( + f"Document with ID '{doc.id}' has a file path '{file_path}' that escapes the " + f"configured root '{root_path}'. Resolved path: '{resolved_file_path}'." + ) + if not resolved_file_path.is_file(): raise ValueError( f"Document with ID '{doc.id}' has an invalid file path '{resolved_file_path}'. " diff --git a/releasenotes/notes/image_utils-path-traversal-guard-951e4271322a3bf5.yaml b/releasenotes/notes/image_utils-path-traversal-guard-951e4271322a3bf5.yaml new file mode 100644 index 00000000000..db829f8fa92 --- /dev/null +++ b/releasenotes/notes/image_utils-path-traversal-guard-951e4271322a3bf5.yaml @@ -0,0 +1,7 @@ +--- +security: + - | + Fixed a path-traversal issue in ``DocumentToImageContent`` and ``LLMDocumentContentExtractor``. + When ``root_path`` is set, a ``file_path`` in document metadata that escapes the root (via + ``../`` or an absolute path) now raises a ``ValueError`` before any file is read, preventing + attacker-controlled metadata from reading arbitrary files on the host. diff --git a/test/components/converters/image/test_image_utils.py b/test/components/converters/image/test_image_utils.py index 6fb37c1edbc..ac455f16420 100644 --- a/test/components/converters/image/test_image_utils.py +++ b/test/components/converters/image/test_image_utils.py @@ -149,6 +149,31 @@ def test_extract_image_source_info_errors(self, test_files_path): with pytest.raises(ValueError, match="missing the 'page_number' key"): _extract_image_sources_info(documents=[document], file_path_meta_field="file_path", root_path="") + def test_extract_image_source_info_rejects_path_traversal(self, test_files_path): + # Attacker-controlled document metadata attempts to escape the configured root. + document = Document(content="test", meta={"file_path": "../../../../../../etc/passwd"}) + with pytest.raises(ValueError, match="escapes the configured root"): + _extract_image_sources_info( + documents=[document], file_path_meta_field="file_path", root_path=str(test_files_path / "images") + ) + + def test_extract_image_source_info_rejects_absolute_outside_root(self, test_files_path): + # Absolute path that lies outside the configured root must be rejected before any IO. + document = Document(content="test", meta={"file_path": "/etc/passwd"}) + with pytest.raises(ValueError, match="escapes the configured root"): + _extract_image_sources_info( + documents=[document], file_path_meta_field="file_path", root_path=str(test_files_path / "images") + ) + + def test_extract_image_source_info_accepts_path_inside_root(self, test_files_path): + # When the resolved path is inside the configured root, processing must succeed. + document = Document(content="test", meta={"file_path": "haystack-logo.png"}) + images_source_info = _extract_image_sources_info( + documents=[document], file_path_meta_field="file_path", root_path=str(test_files_path / "images") + ) + assert len(images_source_info) == 1 + assert images_source_info[0]["mime_type"] == "image/png" + class TestBatchConvertPdfPagesToImages: @patch("haystack.components.converters.image.image_utils._convert_pdf_to_images")