Server Implementation
MCP proxies that pass raw OAuth authorization-server metadata fields (authorization_endpoint, token_endpoint, jwks_uri) into subprocess or child_process calls allow a malicious upstream MCP server to achieve remote code execution on the client.
This is an OS command injection vulnerability (CWE-78) where an OAuth discovery document — a JSON blob fetched from an attacker-controlled MCP server — supplies endpoint URLs that are then handed unmodified to a process spawn API. Even with `shell=False` and list-style argv (which prevents classic shell metacharacter injection), URLs like `--upload-pack=evil` or `--config=...` are interpreted by the spawned binary as flags, giving the attacker arbitrary-flag injection against tools like git, curl, or aws.
This is the exact CVE-2025-6514 (`mcp-remote`) attack class: an MCP proxy fetches `/.well-known/oauth-protected-resource` from a malicious server, then passes `authorization_endpoint` to `subprocess.run(["git", "clone", url])` — and gets RCE the moment the URL is `--upload-pack=evil-script`. MCP-275 is intentionally narrower than the generic command-injection rule (MCP-002): it catches the case where developers correctly avoid `shell=True` and string concatenation but still flow OAuth metadata fields into argv as bare elements. Because OAuth discovery is a routine part of every authenticated MCP integration, every proxy or client that forwards these endpoints is in scope.
import subprocess |
from mcp.server import Server |
srv = Server("proxy") |
def handle_oauth(metadata): |
# VULNERABLE: discovery doc field passed straight into argv — |
# attacker controls the URL, so attacker controls the flag. |
authorization_endpoint = metadata["authorization_endpoint"] |
subprocess.run(["git", "clone", authorization_endpoint], check=True) |
import subprocess |
from urllib.parse import urlparse |
from mcp.server import Server |
srv = Server("proxy") |
ALLOWED_HOSTS = {"auth.example.com", "auth.partner.example.com"} |
def handle_oauth(metadata): |
parsed = urlparse(metadata["authorization_endpoint"]) |
if parsed.scheme != "https": |
raise ValueError("must be https") |
if parsed.hostname not in ALLOWED_HOSTS: |
raise ValueError("untrusted host") |
# `--` separator prevents flag injection even if URL parsing is bypassed. |
subprocess.run(["git", "clone", "--", parsed.geturl()], check=True) |
MCPSafe fires per-occurrence on a single line when ALL three conditions hold: (1) the file is in MCP context (`@modelcontextprotocol/sdk`, `from mcp`, `FastMCP`, or `McpServer`); (2) the same line contains both a process-spawn call (`subprocess.run/Popen/call/check_output`, `child_process.spawn/exec/execFile/spawnSync`, `os.system`, `exec.Command`) AND an OAuth metadata field identifier (`authorization_endpoint`, `token_endpoint`, `jwks_uri`, `registration_endpoint`, `revocation_endpoint`, `userinfo_endpoint`, `device_authorization_endpoint`, `introspection_endpoint`, `end_session_endpoint` — both snake_case and camelCase); (3) no same-line URL-validation marker is present. Markers that silence the finding include `urlparse`, `new URL()`, `validate_url`, `validators.url`, `is_https_url`, `assert_https`, and `shlex.quote`. The rule is disjoint from MCP-002 (shell=True / string concatenation) and MCP-060 v2 (OAuth metadata SSRF) — all three can co-fire informatively when the same file mixes failure modes.
See the full threat catalog for every documented detection.
MCPSafe runs this check — and every other rule in the catalog — on any MCP server you paste in.
Scan now