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_tool to 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/call JSON-RPC request to /mcp with the read-only bearer token, invoking phantom_register_tool with a shell handler (e.g., id)
- Send a second
tools/call request invoking the newly registered tool
- Observe that the shell command executes successfully despite the token only having
read scope
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.
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) insrc/mcp/auth.tsbut never enforces them during request handling. Any authenticated token, regardless of its assigned scopes, can invoke any MCP tool — includingphantom_register_tool, which is intended to requireadminscope and enables arbitrary shell command execution.Impact
A
read-scoped token (intended for monitoring only) can:phantom_register_toolto register a new tool with an arbitrary shell command as its handlerThis 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_SCOPESandgetRequiredScope()are defined insrc/mcp/auth.ts:58-74and correctly map tools to their required scopes.hasScope()at line 38-45 correctly implements scope hierarchy (admin implies all, operator implies read).However,
handleRequest()insrc/mcp/server.ts:115-190authenticates the token and checks rate limits, but never callsgetRequiredScope()orhasScope()before delegating to the transport manager. Theauthresult (containing the token's scopes) is passed through but never consulted for per-tool authorization.Reproduction
phantom token create --scope readtools/callJSON-RPC request to/mcpwith the read-only bearer token, invokingphantom_register_toolwith a shell handler (e.g.,id)tools/callrequest invoking the newly registered toolreadscopeSuggested Fix
Add scope enforcement in
handleRequest()after authentication. The required functions already exist — they just need to be called. Conceptually: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
/triggerendpoint insrc/core/server.tsdoes correctly check foroperatorscope — 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.