diff --git a/mcp/path_safety.yaml b/mcp/path_safety.yaml index 7cc469b..51b40d0 100644 --- a/mcp/path_safety.yaml +++ b/mcp/path_safety.yaml @@ -37,3 +37,35 @@ rules: fix: > Resolve the path with `Path(...).resolve()` and assert it sits under an allowed root before any I/O touches it. Reject paths that escape the root. + + - id: MCP-029 + title: TypeScript MCP tool writes to the filesystem + severity: low + confidence: 0.5 + language: typescript + applies_to: + - mcp_tool + scope: tool + match: + has_write_call: true + explanation: > + This TypeScript MCP tool handler writes to the filesystem. If the path or + the contents derive from the tool's arguments, the caller on the other + side of the protocol chooses both — and that caller is a model, steerable + by a prompt injection carried in retrieved content or an earlier tool + result. How the server is deployed makes this sharper than it looks: a + stdio server is launched as a subprocess by whatever client the user is + running, so it inherits that user's own filesystem permissions rather + than a service account's, and a write escaping its intended directory + reaches the user's home directory, dotfiles, and SSH keys. The server also + cannot see the injection — it receives a well-formed tools/call for a path + it has no way to distinguish from a legitimate one. (Coarse signal — it + flags any filesystem write, not only unnormalized paths, because + TypeScript path-normalization analysis is not yet wired. Confirm the path + is genuinely caller-supplied before acting.) + fix: > + Confine writes to a dedicated working directory: resolve the final path, + verify it stays under that root before writing, and reject absolute paths + and any input containing "..". Where the tool only ever writes generated + names, derive the filename server-side from an id rather than accepting a + path over the protocol at all.