MCP Filesystem Capability Laundering: When a Harmless Tool Can Read Your SSH Keys

Guilliano Molaire Guilliano Molaire 12 min read

Filesystem capability laundering is what happens when an MCP tool that sounds narrow, such as “stage a file in this repository”, can be steered by its arguments to read or write files far outside that scope, like ~/.ssh/id_rsa, because the server process runs with your full user permissions and nothing checks the path at the moment of access. Approving a tool call in your MCP client does not prevent it, since approval only decides whether the call happens and says nothing about what the server process is able to touch. The fix lives in two places: the server has to validate canonical paths against a declared root, and you should run the process in an operating system sandbox so that a bug in that validation cannot reach your credentials.

This post walks through the October 2026 research that gave the problem a name, the failure patterns it found, a checklist you can apply to any server you run, and where identity and authorization (the part Keycloak handles) fit alongside it. If you are looking for a general roundup of MCP risks, the MCP specification’s security best practices already cover that well, so we stay on the narrow question of what a locally run server can read.

What is filesystem capability laundering?

On 15 October 2026, Huang, Tang, Guan, Sun and He published “My Credentials, Please: Detecting Filesystem Capability Laundering in MCP Servers at Scale” on arXiv (arXiv:2610.22301). The authors define a tool as laundering filesystem capability when its declared contract, meaning its name, description and argument schema, does not cover the filesystem effect its arguments can actually produce.

They break the mechanism into three conditions that must all hold. The implemented effect must exceed the scope that the tool contract and the deployment justify, an attacker-influenced argument must be able to choose the filesystem target, and the call must be admitted while the surrounding controls fail to constrain that effect. An attacker usually influences the argument indirectly, for example through text the agent reads in a web page or an issue comment that persuades the model to pass a different path.

The word “laundering” fits because the dangerous read arrives inside a call that looks legitimate and was approved. In the Git case, for example, the user approved a git_add call, the server ran exactly that operation, and the file it staged happened to be a private key outside the repository.

Why is tool approval not a permission boundary?

In a typical local setup, the MCP client starts the server as a child process under your own account, so that process can open any file you can open. The paper states the consequence directly: client-side approval of a tool call grants the underlying server process broad host filesystem permissions rather than restricting it to the declared operation.

This matters because the approval prompt is easy to mistake for a scope check. The prompt tells you which tool is being called and often shows its arguments, but nobody is resolving those arguments to real paths, following symlinks, and comparing the result with an allowed directory. Unless the server code does that work, no component in the setup does it.

The MCP specification addresses the same exposure in its section on local server compromise. It warns that local servers may have direct access to the user’s system, and it recommends that clients run server commands in a sandboxed environment with minimal default privileges and launch them with restricted access to the file system, network and other system resources (Model Context Protocol, Security Best Practices, 2026 revision). The research gives measured examples of why that recommendation matters.

What did the study actually find?

The authors built a pipeline called MCP-FSD and catalogued 21,097 MCP servers from nine registries. Only 7,222 of them (34.2 percent) were local open source servers that reached the measurement pass. The final triage snapshot holds 4,806 servers, and 787 of them (16.4 percent) showed filesystem capabilities even though they were not classified as filesystem servers. In the paper’s own table, 1,345 of the 4,806 (28.0 percent) were labelled as filesystem servers and 2,131 (44.3 percent) showed some filesystem capability flag.

Two details are easy to misread. First, the 787 figure is a screening signal, which means servers worth a closer look, and not 787 confirmed vulnerabilities. Second, the confirmed number is smaller: dynamic testing confirmed 150 vulnerable instances. Each candidate went through three cases with a credential file planted outside the inferred safe boundary: a malicious call without a sandbox, a benign call inside a sandbox, and the same malicious call inside the sandbox. For 136 of the 150, the verdict rests on the planted secret string appearing in the unsandboxed response and not in the sandboxed one, and for the other 14 on other evidence such as write side effects or response changes. In 144 of those 150 (96.0 percent) the payload was a plain traversal path and not a specially formatted container.

The authors submitted 13 findings for coordinated disclosure, and the paper says upstream fixes or CVE assignments for those packages were still pending at its reporting cutoff. The paper also documents two CVEs in the reference Git MCP server (mcp-server-git), CVE-2025-68143 and CVE-2026-27735, and by the authors’ account both are patched. CVE-2025-68143 was first reported by another researcher, Yarden Porat, and the authors contributed independent analysis and a patch. The paper also lists its limits: it only tested servers it could install and run, it validated selected tools and call sequences, and several stages relied on model-generated judgments. Treat the percentages as a measurement of the open source servers they could run and not as a rate for every server in the wild.

What do MCP server security failures look like, from the Filesystem MCP Server to Git?

The paper groups findings into three layers, and the examples are concrete enough to recognize in code you review.

Missing validation. The server takes a path argument and passes it straight to the filesystem. In the study, Python servers more often omitted checks entirely, with a 53.8 percent share of the missing-validation flags (the paper counts these as SARIF emissions, meaning individual reported code locations, not servers).

Bypassable validation. A check exists but can be defeated. The paper cites two earlier CVEs in the reference Filesystem MCP Server as benchmarks: CVE-2025-53110, where a string-prefix check accepted sibling directories that shared the allowed prefix (for example /data/project-secrets passing a check for /data/project), and CVE-2025-53109, a symlink traversal that escaped the allowed directory. Node servers led every bypass-style rule, with between 57 and 63 percent of each rule’s emissions, and the authors point out that the MCP TypeScript SDK ships no path-containment helper, so each server writes its own.

A narrow tool with a wide effect. This is the case that gives the post its title. In the reference Git MCP server, git_init accepted a caller-supplied path with no check that it stayed inside the configured workspace. An attacker-influenced argument naming ~/.ssh could turn that directory into a Git repository, after which git_add and git_diff_staged would return raw private key contents to the agent as diff output. The paper found this one through static analysis and dynamically validated only the git_add case. The fix removed git_init from the server in version 2025.9.25. The second CVE, in git_add, let paths containing ../ be staged from outside the repository, and the fix in version 2026.1.14 delegates staging to the Git command line, which rejects out-of-repository paths.

The Git case is also a staging-then-reading chain across several calls. The paper notes in its limitations that a tool which writes attacker-controlled content that a second tool later reads can launder capability across the pair even when each tool alone respects its boundary, and that its validation did not search for such chains systematically.

What should a per-tool filesystem scope checklist include?

The paper’s mitigation section recommends canonical-path containment, schema validation and handling symlink races at access time, and it floats a protocol extension where each tool would declare a read and write scope in tools/list. That extension does not exist today, so the practical version is a checklist you apply yourself, either when writing a server or when reviewing one.

  1. Declare roots explicitly. Each tool should have a documented set of directories it may read and a separate, usually smaller, set it may write. A tool that takes a path but documents no root is the first thing to question.
  2. Resolve before you compare. Convert the argument to an absolute canonical path (resolving .. and symlinks) and then check that it lies inside a root by comparing path components, not by checking that the string starts with the root’s text. This is the difference that the sibling-prefix CVE exposed.
  3. Check at access time. Resolving a path and then opening it later leaves a window in which a symlink can be swapped. Open the file with the resolved path, or use the operating system’s no-follow options where available.
  4. Separate read from write. A tool that only needs to read should not share a code path, or a process permission, with one that writes.
  5. Reject surprising arguments. Absolute paths, home directory shortcuts and parent-directory segments in an argument that is supposed to be relative are all worth refusing outright.
  6. Look at what a tool returns. A “diff” or “log” tool that hands back file contents is a read primitive, so it needs the same containment as an explicit read tool.

How do you sandbox a local MCP server?

Containment in the server code can have bugs, so the second layer is to make sure that a bug cannot reach anything valuable. In the paper’s three-case test on the Git git_add issue, the sandboxed malicious call returned a permission-denied error while the benign call on an in-scope file succeeded, and across all 150 confirmed instances the sandboxed run passed that gate. All of the dynamic tests ran in nono v0.43.0, a macOS Seatbelt sandbox, so the evidence is for that one backend. The authors note that enforcement strength depends on the sandbox backend, and they reserve “kernel-enforced” for mechanisms such as Landlock and Seatbelt. The container and unprivileged-user options below follow the MCP specification’s recommendation of platform-appropriate sandboxing (containers, chroot, application sandboxes) and were not tested by the paper.

In practice this comes down to a few habits:

  • Run the server in a container, a dedicated unprivileged user account, or an application sandbox, and mount only the working directory it needs, read-only where possible.
  • Keep credentials out of reach of the process. Do not run servers under the account that holds your SSH keys, cloud credentials and browser profile, and pass a server only the specific environment variables it needs.
  • Turn off network access for servers that have no reason to reach the internet. This only stops the server process from sending data out itself: file contents are returned to the agent in the MCP response and can leave through other tools the agent has, so path containment and the filesystem sandbox are what carry the weight.
  • Prefer a short-lived, narrowly scoped token over a long-lived key when a server must call another service.

Where does identity fit in?

Filesystem scope is not authorization, and it helps to keep the two questions apart. Filesystem containment answers “which files can this process touch on this machine”, and it is enforced by the server code and the host sandbox. An identity provider cannot sandbox a process running on your laptop, and nothing about Keycloak changes what a local stdio server can open.

What an identity provider does answer is who or what is allowed to call a tool in the first place. For servers that are reachable over HTTP, Keycloak can act as the authorization server. It can issue access tokens whose audience is the MCP server (through an Audience mapper, or through the experimental resource indicators feature) and whose scopes differ per client, depending on how each client is registered, for example through dynamic client registration or the experimental client ID metadata documents support. Keycloak never sees the individual tool calls, so the MCP server has to do the enforcement, for instance by refusing write tools when the token lacks a write scope. The Keycloak guide to integrating with MCP lists which MCP specification versions and standards it supports. This narrows who can steer a tool’s arguments and lets you tell which client made a call, which is a useful second layer even though it does not stop a path traversal.

Our earlier posts cover that layer in detail: securing MCP servers with Keycloak and OAuth 2.0, dynamic client registration for MCP, the MCP authorization decision tree, and issuer pinning after the MCP Python SDK credential theft issue. The decision tree also explains why local stdio servers are expected to skip OAuth and read credentials from the environment, which is exactly the case where host sandboxing carries the weight. If you front several servers with a single entry point, an MCP gateway can enforce which tools each client may call, and non-human identity for AI agents covers how to give agents their own identities.

How can you review an MCP server before installing it?

The most direct check on a server you are considering is a planted file outside its workspace, which is the same technique the researchers automated at scale. The steps below are a manual version.

  1. Read the tool list and mark every tool that accepts a path, filename, directory or repository argument, including ones whose names suggest something else, such as build, log or diff tools.
  2. Search the source for where those arguments reach the filesystem, and look for canonical path resolution and a root comparison. If you find a string startsWith check or no check at all, treat the server as unsafe for sensitive hosts.
  3. Create a test directory and, outside it, a file containing a harmless marker string. Start the server with the test directory as its workspace, then ask for the outside file using ../, an absolute path, and a symlink inside the workspace that points to it. If the marker appears in any response, the server does not contain its tools.
  4. Repeat the test from inside your sandbox to confirm the operating system blocks the read even when the server does not.
  5. Check how the project responds to reports. A clear security policy and recent fixes are better signs than the absence of known issues.

Run these tests only against your own machine and your own planted files.

Frequently asked questions

Does approving each tool call protect me from capability laundering?

No. Approval decides whether a call runs. The paper’s threat model for typical local deployments is that approving a call leaves the server process with the broad filesystem permissions of the account it runs under, so the limit has to come from path checks inside the server and an operating system sandbox (arXiv:2610.22301, 2026).

Are 16.4 percent of MCP servers vulnerable?

Not exactly. The 16.4 percent figure (787 of 4,806 triaged servers) is the share that showed filesystem capabilities without being classified as filesystem servers, which marks them for closer review. The dynamic testing confirmed 150 vulnerable instances, and the authors only tested servers they could install and run.

Is this the same as prompt injection?

They are related but different. Prompt injection is one way an attacker can influence the arguments the model passes to a tool. Capability laundering is the resulting server-side effect, and missing or flawed path checks are what turn a manipulated argument into a file read (arXiv:2610.22301, 2026).

Can Keycloak or OAuth scopes prevent a path traversal?

No, because they act at a different layer. Scopes and audience restricted tokens control which clients may call which tools on an HTTP-accessible server, and they reduce who can steer a tool. Preventing the traversal itself requires path containment in the server and sandboxing on the host.

Which servers should I check first?

Start with any server that can run commands or touches a repository, document store or build output, since the study’s best-known cases came from a Git server. Check anything that runs under an account holding SSH keys or cloud credentials before anything else.

Summary

The research puts numbers on something the MCP specification already warns about: a local server runs with your permissions, and tool approval does not narrow them. For each server you run, confirm that it resolves and checks canonical paths against a declared root, run it in a sandbox that only mounts what it needs, and keep your credentials outside the account it runs under. For servers you expose over HTTP, you can then use OAuth scopes and audience restricted tokens, issued by Keycloak or another authorization server and checked by the MCP server, to control which clients may call which tools. The primary source is the paper itself: My Credentials, Please: Detecting Filesystem Capability Laundering in MCP Servers at Scale (arXiv, 15 October 2026).

Identity you do not have to patch yourself

Skycloak is a managed identity platform for SSO, MFA, SCIM and audit logs. When an advisory lands, the fix is ours to ship: Keycloak 26.7.2 closed an account takeover through password reset and customers had it the same day upstream published it.

Guilliano Molaire
Written by
Founder

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

Start Free Trial Talk to Sales
© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman