high · 7.5Aug 19, 2026

claude-faf-mcp Arbitrary File Read/Write via Unconfined Path Argument

Shubham Kandhare
Security Engagement Manager, SecureLayer7

An MCP tool in claude-faf-mcp accepts a caller-supplied file path and passes it straight to the filesystem with no directory boundary, letting any MCP client or prompt-injected agent read SSH keys…

Packageclaude-faf-mcp
Ecosystemnpm
Affected<= 5.7.1
Fixed in5.7.2
claude-faf-mcp Arbitrary File Read/Write via Unconfined Path Argument

The problem

Every tool that calls the shared getProjectPath() helper, plus the general-purpose faf_read and faf_write tools, accepted a caller-controlled path argument and passed it through ~-expansion and path.resolve() directly into a filesystem read or write.

No allowlist, no confinement to the project root, and no symlink canonicalization were applied.

Because the server runs over stdio and is reachable from the LLM itself, a prompt-injected payload in any attacker-controlled content (a README, a ticket, a web page the agent browses) could issue a crafted tool call and read files limited only by OS permissions: SSH private keys, AWS credentials, .env files, /etc/passwd, and more. faf_write extended the risk to arbitrary out-of-project writes.

Proof of concept

A working proof-of-concept for this issue in claude-faf-mcp, with the exact payload below.

json
// Sent as a standard MCP tools/call request over stdio JSON-RPC
// Reads ~/.ssh/id_rsa via faf_read (absolute path, no traversal needed)
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "faf_read",
    "arguments": {
      "path": "~/.ssh/id_rsa"
    }
  }
}

// Alternatively, ../  traversal from a project working directory
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "faf_read",
    "arguments": {
      "path": "../../../.aws/credentials"
    }
  }
}

The root cause is a missing confinement step. The code expanded ~ and called path.resolve(), which both turn relative and home-relative paths into fully qualified absolute paths, and then handed that result straight to fs.readFile / fs.writeFile. There was no check that the resolved path fell inside the project root.

The 5.7.2 patch introduced safe-path.ts, which canonicalizes paths through symlinks (closing symlink-bypass), then asserts the result starts with an allowlisted root (cwd, system temp, or FAF_ALLOWED_ROOTS). Context-file tools additionally enforce that the target has a .faf or .fafm extension, so even a path inside the project root cannot be used to read arbitrary files.

A central guard in callTool() runs this check before any tool handler is invoked. CWE-22 (path traversal) and CWE-73 (external control of file name) both apply; the information-disclosure impact maps to CWE-200.

The fix

Upgrade to claude-faf-mcp@5.7.2: npm install -g claude-faf-mcp@5.7.2. If an immediate upgrade is not possible, set the FAF_ALLOWED_ROOTS environment variable to a single trusted project directory and run the server only against local, trusted projects. The allowlist is enforced by the patched safe-path.ts guard in callTool().

Reported by Zhihao Zhang (Worcester Polytechnic Institute) — coordinated disclosure of the same class of issue in the sibling grok-faf-mcp server prompted the maintainers to audit and find this instance.

References: [1][2][3]

Related research