Skip to content

Commit 5c659a2

Browse files
camgrimsecsjrl
andauthored
fix: reject path-traversal payloads in document metadata (#11787)
Co-authored-by: Sebastian Husch Lee <10526848+sjrl@users.noreply.github.com>
1 parent 715ffcb commit 5c659a2

3 files changed

Lines changed: 44 additions & 0 deletions

File tree

haystack/components/converters/image/image_utils.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,18 @@ def _extract_image_sources_info(
248248
)
249249

250250
resolved_file_path = Path(root_path, file_path)
251+
252+
# When root_path is set, ensure the resolved path stays within it to block path-traversal
253+
# payloads (e.g. "../../etc/passwd") coming from document metadata.
254+
if root_path:
255+
resolved_file_path = resolved_file_path.resolve()
256+
resolved_root = Path(root_path).resolve()
257+
if not resolved_file_path.is_relative_to(resolved_root):
258+
raise ValueError(
259+
f"Document with ID '{doc.id}' has a file path '{file_path}' that escapes the "
260+
f"configured root '{root_path}'. Resolved path: '{resolved_file_path}'."
261+
)
262+
251263
if not resolved_file_path.is_file():
252264
raise ValueError(
253265
f"Document with ID '{doc.id}' has an invalid file path '{resolved_file_path}'. "
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
security:
3+
- |
4+
Fixed a path-traversal issue in ``DocumentToImageContent`` and ``LLMDocumentContentExtractor``.
5+
When ``root_path`` is set, a ``file_path`` in document metadata that escapes the root (via
6+
``../`` or an absolute path) now raises a ``ValueError`` before any file is read, preventing
7+
attacker-controlled metadata from reading arbitrary files on the host.

test/components/converters/image/test_image_utils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,31 @@ def test_extract_image_source_info_errors(self, test_files_path):
149149
with pytest.raises(ValueError, match="missing the 'page_number' key"):
150150
_extract_image_sources_info(documents=[document], file_path_meta_field="file_path", root_path="")
151151

152+
def test_extract_image_source_info_rejects_path_traversal(self, test_files_path):
153+
# Attacker-controlled document metadata attempts to escape the configured root.
154+
document = Document(content="test", meta={"file_path": "../../../../../../etc/passwd"})
155+
with pytest.raises(ValueError, match="escapes the configured root"):
156+
_extract_image_sources_info(
157+
documents=[document], file_path_meta_field="file_path", root_path=str(test_files_path / "images")
158+
)
159+
160+
def test_extract_image_source_info_rejects_absolute_outside_root(self, test_files_path):
161+
# Absolute path that lies outside the configured root must be rejected before any IO.
162+
document = Document(content="test", meta={"file_path": "/etc/passwd"})
163+
with pytest.raises(ValueError, match="escapes the configured root"):
164+
_extract_image_sources_info(
165+
documents=[document], file_path_meta_field="file_path", root_path=str(test_files_path / "images")
166+
)
167+
168+
def test_extract_image_source_info_accepts_path_inside_root(self, test_files_path):
169+
# When the resolved path is inside the configured root, processing must succeed.
170+
document = Document(content="test", meta={"file_path": "haystack-logo.png"})
171+
images_source_info = _extract_image_sources_info(
172+
documents=[document], file_path_meta_field="file_path", root_path=str(test_files_path / "images")
173+
)
174+
assert len(images_source_info) == 1
175+
assert images_source_info[0]["mime_type"] == "image/png"
176+
152177

153178
class TestBatchConvertPdfPagesToImages:
154179
@patch("haystack.components.converters.image.image_utils._convert_pdf_to_images")

0 commit comments

Comments
 (0)