Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions haystack/components/converters/image/image_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}'. "
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
25 changes: 25 additions & 0 deletions test/components/converters/image/test_image_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading