Skip to content

ReadFileTool shells out on ranged reads and interpolates untrusted path #5530

@failuresmith

Description

@failuresmith

ReadFileTool shells out for ranged reads (start_line > 1 or end_line is set) instead of reading the file directly.

cmd = f"cat -n '{path}' | sed -n '{sed_range}p'"
res = await self._environment.execute(cmd)

As a result, the ranged-read path is not a pure file-read operation, and paths containing shell-sensitive characters can be interpreted unexpectedly.

Steps to Reproduce:

  1. Install ADK.
  2. Run the minimal reproduction below.
  3. Call ReadFileTool with start_line=2 and a path containing a single quote.
  4. Observe that the ranged-read path invokes shell execution instead of direct file I/O.

Expected Behavior:
ReadFileTool should always read file contents directly and should not invoke shell commands based on the requested line range.

Observed Behavior:
When start_line or end_line is provided, ReadFileTool builds and runs a shell command using the provided path.

Environment Details:

  • ADK Library Version (pip show google-adk): 1.31.0
  • Desktop OS:** Linux
  • Python Version (python -V): Python 3.14.4

Model Information:

  • Are you using LiteLLM: No
  • Which model is being used: N/A

🟡 Optional Information

Regression:
Unknown / N/A

Logs:
text file.

No special logs required. The issue is reproducible from the current
ReadFileTool implementation.

Screenshots / Video:
N/A

Additional Context:
This looks fixable by removing the shell-based ranged-read path and handling line slicing entirely in Python.

Minimal Reproduction Code:
Please provide a code snippet or a link to a Gist/repo that isolates the issue.

import asyncio
from pathlib import Path
import tempfile

from google.adk.environment._local_environment import LocalEnvironment
from google.adk.tools.environment._tools import ReadFileTool


async def main():
  with tempfile.TemporaryDirectory() as td:
    env = LocalEnvironment(working_dir=Path(td))
    await env.initialize()

    target = Path(td) / "sample.txt"
    target.write_text("line1\nline2\nline3\n", encoding="utf-8")

    marker = Path(td) / "marker.txt"
    # ReadFileTool is expected to treat `path` as data, not as shell syntax.
    # This payload closes the quoted filename used by the ranged-read path,
    # injects an extra command, and reopens the quote to keep the shell happy.
    injected_path = f"sample.txt'; touch {marker}; echo '"

    tool = ReadFileTool(env)
    result = await tool.run_async(
        # `start_line=2` is important because it triggers the code path that
        # shells out through `cat ... | sed ...` instead of doing pure Python
        # file I/O.
        args={"path": injected_path, "start_line": 2},
        tool_context=None,
    )

    print(result)
    # If this prints True, a file-read API caused an unintended side effect.
    # That demonstrates the bug: the path was interpreted by the shell.
    print("marker exists:", marker.exists())
    await env.close()


asyncio.run(main())

How often has this issue occurred?:

  • Always (100%)

Metadata

Metadata

Labels

tools[Component] This issue is related to tools

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions