All posts
Agentic AI

An agent with a read_file tool can read any file

The filesystem tools from the knowledge-base chapter are what let an agent explore a project: list the tree, read a file, unpack an archive. They are genuinely useful, and they share one property worth looking at hard.

Each one takes a path from the model and acts on it. A tool that reads whatever path it is given can read more than you meant, and that is one of the oldest bugs in software, now with an agent holding the keyboard.

The tool does exactly what it says

Here is the read tool, in its honest, minimal form.

@tool
def read_file(path: str) -> str:
    """Read a text file and return its contents."""
    return Path(path).read_text()

Nothing there constrains path. The tool reads whatever it is handed, which is precisely what makes it work for exploring a project. It is also what lets it read /etc/passwd, or ../../.env, or any file the process has permission to open.

The model chooses the path. And the model, from earlier in this series, takes direction from untrusted input: a retrieved chunk, a tool result, a file it just read. So the path is not chosen in a vacuum. It is chosen by a model that may be reading an attacker's text at the time.

Two old bugs, one new trigger

Path traversal. A path argument that is not confined to an intended directory can climb out of it. ../ a few times and the tool is reading configuration, credentials, other tenants' data, anything the process can reach. The agent does not need to be "hacked" for this; it needs to be convinced, and a document that says "check the config in ../../../etc for the setting" is a convincing-enough sentence to a helpful model.

Zip-slip. Extraction is the write-side twin. A malicious archive can contain an entry named ../../../home/user/.bashrc, and a naive extractall writes it exactly there, outside the folder you meant to extract into. Overwrite the right file and you have gone from "unpack this attachment" to changing what runs on the machine.

# the innocent-looking line that trusts the archive's own filenames
with zipfile.ZipFile(zip_path) as zf:
    zf.extractall(extract_to)          # entries can name paths outside extract_to

Neither bug is new. Both have been in secure-coding guides for years. What changed is the trigger: instead of a crafted HTTP request, the input is a path a language model produced while reading content it did not vet.

A path argument reads outward; a crafted archive writes outward. Both escape the intended directory.A path argument reads outward; a crafted archive writes outward. Both escape the intended directory.

The fix is confinement, in code

The model cannot be trusted to stay inside the lines, because the thing steering it is exactly what an attacker influences. So the boundary goes in the tool, not in the prompt.

ControlWhat it stops
Resolve the path and confirm it stays under an allowed rootPath traversal, ../ and absolute paths that climb out
Validate every archive entry before extracting itZip-slip, entries that name a path outside the target
Run the agent as a least-privilege userBounds what any file tool can reach, however it is called
Sandbox the filesystem the agent seesThe agent's "any file" is a small, disposable directory, not the host
Deny by default, allow-list what it may touchA tool that can read three folders cannot read the fourth
Log every path read and writtenAn escape attempt you can see is one you can respond to

Confine the path to an allowed root, validate archive entries, and sandbox the whole thing.Confine the path to an allowed root, validate archive entries, and sandbox the whole thing.

The first two rows are the specific fixes and they are short: resolve the requested path and check it is still under your allowed root before opening it; check each archive entry's destination is inside the extraction folder before writing it. The rest is defence in depth, so that even a tool called with a bad path meets a process that cannot reach much and a filesystem that barely exists.

Why the prompt cannot fix this

It is tempting to add "never read files outside the project directory" to the instructions and move on. That is a policy, not a control, and this series has made the distinction before. The instruction sits in the same buffer as the attacker's text, and the attacker's text can be more persuasive in the moment. A confinement check in read_file does not care how persuasive anything was. The function resolves the path, sees it climbs out, and refuses. That is a boundary the model cannot argue with.

What this touches

For the frameworks, this sits on the MAESTRO Agent Frameworks and Deployment layers. The read side maps to T2 Tool Misuse, an agent driven to use a tool beyond its intended scope while operating within its granted permissions. The write side, zip-slip overwriting files that later execute, maps to T11 Unexpected RCE and Code Attacks. In the OWASP Agentic Top 10 the anchors are ASI02 and, for the code-execution side, ASI05.

On the regulatory side, framed as scope rather than a citation: unauthorised access to data and to systems is covered by every GCC rulebook, and access control is a foundational obligation. An agent whose file tools can reach beyond their intended scope is an access-control failure those rules already govern, even though they pictured a user account rather than a language model choosing a path. Which obligations apply depends on your regulator and architecture, and that mapping is an advisory estimate until someone checks it properly.

The part worth keeping

Filesystem tools are the right way to let an agent explore structured data, and the previous post built them for good reason. Nothing here says do not give an agent files.

The point is that a path from the model is untrusted, the same as any other model-influenced value, and the tool has to confine it in code. Resolve and check the path, validate archive entries, run least-privilege, sandbox the view. Do that and the agent explores freely inside a box it cannot climb out of. Skip it and read_file means read any file, which is not the tool you thought you shipped.