-
Notifications
You must be signed in to change notification settings - Fork 104
MCP per-tool scope enforcement not implemented — read-only tokens can invoke admin tools #20
Description
Severity: Critical
Type: Broken Access Control (CWE-285: Improper Authorization)
Affected versions: v0.18.1 (current) and likely all prior versions with MCP support
Summary
The MCP server defines per-tool scope requirements (read, operator, admin) in src/mcp/auth.ts but never enforces them during request handling. Any authenticated token, regardless of its assigned scopes, can invoke any MCP tool — including phantom_register_tool, which is intended to require admin scope and enables arbitrary shell command execution.
Impact
A read-scoped token (intended for monitoring only) can:
- Call
phantom_register_toolto register a new tool with an arbitrary shell command as its handler - Invoke that tool to execute the command on the host
This provides remote code execution to any holder of any valid MCP token. In deployments where read-only tokens are distributed to dashboards, monitoring systems, or peer Phantom instances, this expands the attack surface well beyond what the token issuer intended.
Root Cause
TOOL_SCOPES and getRequiredScope() are defined in src/mcp/auth.ts:58-74 and correctly map tools to their required scopes. hasScope() at line 38-45 correctly implements scope hierarchy (admin implies all, operator implies read).
However, handleRequest() in src/mcp/server.ts:115-190 authenticates the token and checks rate limits, but never calls getRequiredScope() or hasScope() before delegating to the transport manager. The auth result (containing the token's scopes) is passed through but never consulted for per-tool authorization.
Reproduction
- Generate a read-only MCP token via
phantom token create --scope read - Send a
tools/callJSON-RPC request to/mcpwith the read-only bearer token, invokingphantom_register_toolwith a shell handler (e.g.,id) - Send a second
tools/callrequest invoking the newly registered tool - Observe that the shell command executes successfully despite the token only having
readscope
Suggested Fix
Add scope enforcement in handleRequest() after authentication. The required functions already exist — they just need to be called. Conceptually:
// After authentication and rate limiting, before delegating to transport:
const body = await req.clone().json();
if (body?.method === "tools/call" && body?.params?.name) {
const requiredScope = getRequiredScope(body.params.name);
if (!this.auth.hasScope(auth, requiredScope)) {
return Response.json(
{ jsonrpc: "2.0", error: { code: -32001, message: `Insufficient scope: requires ${requiredScope}` }, id: body.id },
{ status: 403 }
);
}
}Additionally, getRequiredScope() currently defaults unknown tool names to "read" (line 73). Dynamically registered tools would fall through to this default, meaning any authenticated user could invoke them. Consider defaulting to "operator" for unknown tools.
Additional Context
The /trigger endpoint in src/core/server.ts does correctly check for operator scope — this appears to be the intended pattern that was missed for the MCP tool invocation path.
Identified during a security audit with assistance from Claude.