Skip to content

Arbitrary file read via ImageDocument.metadata["file_path"] in image_documents_to_base64 #21512

Description

@etairl

Bug Description

Summary

image_documents_to_base64 in llama-index-core opens any path supplied via ImageDocument.metadata["file_path"] and base64-encodes the bytes — with no allow-listed root, no symlink check, and no MIME validation. Applications that build ImageDocument instances from user-influenced data (a common pattern: tool arguments, JSON request bodies, deserialized documents) effectively expose an arbitrary-file-read primitive whose output is forwarded to the configured multimodal LLM.

Affected code

llama-index-core/llama_index/core/multi_modal_llms/generic_utils.py, lines 65-86:

for image_document in image_documents:
    if image_document.image:
        image_encodings.append(image_document.image)
    elif image_document.image_path and os.path.isfile(image_document.image_path):
        image_encodings.append(encode_image(image_document.image_path))
    elif (
        "file_path" in image_document.metadata
        and image_document.metadata["file_path"] != ""
        and os.path.isfile(image_document.metadata["file_path"])
    ):
        image_encodings.append(encode_image(image_document.metadata["file_path"]))
    elif image_document.image_url:
        response = requests.get(image_document.image_url, timeout=(60, 60))
        ...

encode_image then performs an unguarded open(image_path, "rb") and base64-encodes the bytes.

metadata is a free-form dict on Document, so no validator gates it. The is_image_pil check on ImageDocument.__init__ only fires for the image_path= constructor argument, not for metadata. The library's own tests use this construction pattern (tests/multi_modal_llms/test_generic_utils.py:67 instantiates ImageDocument(metadata={"file_path": "test.jpg"})).

Impact

In RAG / multimodal-agent applications that ingest user-influenced input into ImageDocument (or that allow tool calls to construct one), an attacker can read arbitrary files the process has access to — /etc/passwd, ~/.aws/credentials, /var/run/secrets/kubernetes.io/serviceaccount/token, application .env files, etc. The bytes are returned in image_encodings and assigned to ImageDocument.image, then forwarded to the LLM provider — and typically echoed back to the user when the model is asked to "describe this image."

Suggested fix

Restrict accepted paths to an explicit allow-listed root, reject symlinks, and validate MIME/size before encoding:

from pathlib import Path

ALLOWED_ROOT = Path("/var/app/uploads").resolve()

def _safe_image_path(p: str) -> Path:
    resolved = Path(p).resolve(strict=True)
    if ALLOWED_ROOT not in resolved.parents and resolved != ALLOWED_ROOT:
        raise ValueError("image path escapes allowed directory")
    if resolved.is_symlink():
        raise ValueError("symlinks not allowed")
    return resolved

A more defensive option: stop honoring metadata["file_path"] as a path source entirely, and require callers to set image_path (which goes through the existing is_image_pil check) or pre-encoded image.

Scope note

I'm aware the project's security policy classifies path-traversal-from-untrusted-paths as out of scope for the Huntr bounty (treated as application-layer responsibility). Filing this as a public bug rather than as a security report — many users may not realize that metadata["file_path"] bypasses the constructor's image-validation check.

Version

0.14.21

Steps to Reproduce

from llama_index.core.schema import ImageDocument
from llama_index.core.multi_modal_llms.generic_utils import image_documents_to_base64

# No image_path, no image_url — only metadata['file_path'].
# This branch is not gated by is_image_pil() validation.
doc = ImageDocument(metadata={"file_path": "/etc/passwd"})

encoded = image_documents_to_base64([doc])
print(encoded)  # base64 of /etc/passwd

The same primitive works against ~/.aws/credentials, /var/run/secrets/kubernetes.io/serviceaccount/token, application .env files — anything the process can read.

In a real deployment this is reachable any time user input flows into an ImageDocument's metadata (tool-call arguments, JSON request bodies, deserialized documents, third-party feeds). The encoded bytes end up in ImageDocument.image via set_base64_and_mimetype_for_image_docs and are forwarded to the multimodal LLM provider, where they are typically echoed back when the model is asked to describe the "image."

Relevant Logs/Tracebacks

Scanned automatically with https://github.com/etairl/Probus

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingtriageIssue needs to be triaged/prioritized

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions