Critical RCE Vulnerability CVE-2025-11953 Puts React Native Developers at Risk
The JFrog Security Research team recently discovered and disclosed CVE-2025-11953 – a critical (CVSS 9.8) security vulnerability affecting the extremely popular @react-native-community/cli NPM package that has approximately 2M weekly downloads.
The vulnerability allows remote unauthenticated attackers to easily trigger arbitrary OS command execution on the machine running react-native-community/cli’s development server, posing a significant risk to developers.
React Native is a popular framework for building cross-platform mobile apps using JavaScript. The vulnerability is in a package which is part of the broader React Native Community CLI project that was extracted from the core react-native codebase a few years ago to improve maintainability. The CLI is a collection of command line tools that help developers build React Native mobile applications. It is officially used for creating React Native mobile apps without using a framework, as well as React Native for Windows, React Native for macOS, and more.
Unlike typical vulnerabilities in development servers that are only exploitable from a developer’s local machine, a second security issue that the team spotted in React Native’s core codebase, exposes the development server to external network attacks – making the former vulnerability a highly critical issue.
We would like to thank Meta’s security team and engineers for promptly handling the CVE-2025-11953 vulnerability.
Who is affected by CVE-2025-11953?
Developers who initiated their React Native project with a vulnerable version of @react-native-community/cli, and run the Metro development server via one of the following or similar commands are vulnerable to CVE-2025-11953:
npm start
npm run [start|android|ios|windows|macos]
npx react-native [start|run-android|run-ios|run-windows|run-macos]
npx @react-native-community/cli [start|run-android|run-ios|run-windows|run-macos]
react-native’s development server (Metro) running
While the vulnerability is exploitable by default when initiating a react-native project using @react-native-community/cli, it is important to understand that not every developer that has this library installed as a dependency is necessarily vulnerable.
Specifically, developers who use React Native with a framework that doesn’t use Metro as the development server are typically not vulnerable.
The vulnerability directly affects the @react-native-community/cli-server-api package, versions 4.8.0 to 20.0.0-alpha.2, and is fixed since version 20.0.0.
Here is how to check whether the vulnerable package exists in a specific NodeJS project:
cd <Project Folder>
npm list @react-native-community/cli-server-api
The package may also be globally installed on your system, which can be checked by running:
npm list -g @react-native-community/cli-server-api
Note that the affected package is commonly bundled with @react-native-community/cli in matching versions. As a result, projects using @react-native-community/cli versions 4.8.0 through 20.0.0‑alpha.2 are likely to include vulnerable versions of @react-native-community/cli-server-api.
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.
How can CVE-2025-11953 be mitigated?
Performing the following steps will mitigate CVE-2025-11953:
- Update @react-native-community/cli-server-api to version 20.0.0 or higher, which includes a fix for this vulnerability, in each of your react-native projects. This is the recommended solution.
- For improved security, or if upgrading is not possible, bind the development server to the localhost interface explicitly, by including the “–host 127.0.0.1” flag, per the examples below:
npx react-native start --host 127.0.0.1
npx @react-native-community/cli start --host 127.0.0.1
CVE-2025-11953 Summary
The Metro development server, which is opened by @react-native-community/cli, binds to external interfaces by default (a small security issue in itself). The server’s /open-url endpoint handles a POST request that includes a user-input value that is passed to the unsafe open() function provided by the open NPM package, which will cause OS command execution.
This allows an unauthenticated network attacker to send a POST request to the server and execute arbitrary shell commands with attacker-controlled parameters.

CVE-2025-11953 Technical Details
When creating a React Native project, developers can choose to use an existing framework, such as Expo, which uses the framework’s CLI and is not vulnerable, or create their app without a framework – as we’ll choose here – if they prefer to write their own framework or have certain constraints that a certain framework interferes with.
The development process typically begins by using the @react-native-community/cli to initialize a new project with the command npx @react-native-community/cli init MyApp, which sets up the necessary project structure, dependencies, and configuration files. During active development, developers rely on the CLI to run the Metro development server (using commands like npx react-native start) which serves the JavaScript bundle to the running app (on an emulator or mobile device) and enables essential development features like hot reloading and fast refresh for rapid iteration.
While running npx react-native start – react-native’s cli.js forwards the command to @react-native-community/cli package (an optional dependency of react-native), which runs its build\index.js::setupAndRun() function. This function loads the CLI commands (i.e. start command) by calling loadConfigAsync, which finds and uses the node-modules/react-native/react-native.config.js file in the process.
const commands = [];
const {
bundleCommand,
startCommand,
} = require('@react-native/community-cli-plugin');
commands.push(bundleCommand, startCommand);
node-modules/react-native/react-native.config.js – adds the startCommand to an exported commands list that is then added to the CLI commands list.
The react-native.config.js file imports startCommand from @react-native/community-cli-plugin – that is responsible for running the development server.
import runServer from './runServer';
/* ... */
const startCommand: Command = {
name: 'start',
func: runServer,
description: 'Start the React Native development server.',
Snippet from start command’s index.js file
When the start command runs – it invokes runServer.js:
/* ... */
import {createDevServerMiddleware} from './middleware'; /* *** 1 *** */
/* ... */
/* *** 2 *** */
const { middleware: communityMiddleware } = createDevServerMiddleware({
host: hostname,
port,
watchFolders,
});
/* ... */
/* *** 3 *** */
await Metro.runServer(metroConfig, {
host: args.host,
secure: args.https,
secureCert: args.cert,
secureKey: args.key,
unstable_extraMiddleware: [communityMiddleware, middleware],
websocketEndpoints: {
...communityWebsocketEndpoints,
...websocketEndpoints,
},
});
Simplified version of runServer.js
runServer.js does the following:
- Imports the createDevServerMiddleware function from ./middleware, which imports it from @react-native-community/cli-server-api’s index.ts.
- Runs createDevServerMiddleware, which creates middleware – a function added to an HTTP server that intercepts incoming requests, adds functionality, and then passes them to the next handler. In our case, the added handler that interests us is /open-url, which is handled by the openURLMiddleware handler. The middleware is set to the communityMiddleware variable.
- Runs the Metro server, with communityMiddleware as a parameter. This is the CLI’s development server.
Now let’s look at the openURLMiddleware handler that was added to the server as part of communityMiddleware:
async function openURLMiddleware(req: IncomingMessage, ...) {
if (req.method === 'POST') {
/* ... */
const {url} = req.body as {url: string};
await open(url);
/* ... */
}
next();
}
export default connect().use(json()).use(openURLMiddleware);
Simplified snippet of openURLMiddleware function from openURLMiddleware.ts
This function is called with an HTTP request req parameter, that is a JSON POST request, with a url string field. It takes the value of the url’s field, which is an unsanitized string coming directly from the user, and uses it as a parameter to open() call.
- The
open()function is imported from the ‘open’ npm package version 6.4.0. open(url)does the following on Windows machines:- Sets
commandto run to be “cmd” - Builds the
cliArgumentsarray:['/c', 'start', '""', '/b'] - Escapes & with ^ in the
target stringwhich is the url we supplied. - Adds
targetstring to thecliArguments - Runs the following command:
childProcess.spawn(command, cliArguments, childProcessOptions);
- Sets
While the start cmd command can accept a single URL param and open it inside the default browser due to its HTTP URI scheme, it also accepts the following format for running commands:
start “title” /b command optional_command_parameters
In the case of open(“calc.exe”), the following new process will be spawned:
cmd /c start “” /b calc.exe
Resulting in execution of calc.exe.
For running any arbitrary command, we could set open’s url parameter to a cmd /c <payload> command. For example, resulting in spawning the following:
cmd /c start "" /b cmd /c echo abc > c:\temp\pwned.txt
A new pwned.txt file created in C:\temp as a result of our cmd.exe /c command – proving the arbitrary code execution succeeded.
Attack surfaces on different operating systems
Unlike Windows, as shown above, the open package uses different code paths on macOS and Linux, executing open <target> and xdg-open <target> respectively. In Unix-like OSs, the spawned process expects each argument to be a separate string in cliArguments, which is unlike Windows where the spawned process expects a single lpCommandLine argument string that is joined before the CreateProcess invocation.
Furthermore, both commands are executed without a shell. Combined with the fact that we only control a single string (url) – these factors will not allow straightforwardly running arbitrary commands on macOS and Linux.
Both xdg-open and open determine whether the input string is a file or URI – and act (dispatch) accordingly.
The attack surface for achieving code execution in these OSs may depend on the system’s configuration and typically comprises of:
- Finding a URI scheme whose handler can help achieve a step in the exploitation (either as a feature or exploit a vulnerability in the handler itself).
- Executing a local file by using a regular or file:// URI scheme. (optionally could be combined with a file-dropper primitive).
- Executing a remote file on the attacker’s server by using smb:// ,dav:// URI schemes or similar. (will likely trigger security dialogues asking for the victim user’s approval)
In conclusion – arbitrary OS command execution on these platforms may be achievable with further research.
Shouldn’t the dev server only bind to localhost?
As we’ve shown earlier, while running the Metro development server, it explicitly states that it is starting using the localhost interface:
However – in reality, we can see the server is listening on all interfaces! (0.0.0.0 and IPv6 [::]):
As mentioned previously, this is due to another vulnerability that we’ve discovered and disclosed in @react-native/community-cli-plugin, which was regarded as “Informative”.
By running npx react-native start, the code reaches the runServer.js::runServer function:
async function runServer(
_argv: Array,
cliConfig: Config,
args: StartCommandArgs,
) {
/* ... */
const hostname = args.host?.length ? args.host : 'localhost'; // *** 1 ***
/* ... */
const protocol = args.https === true ? 'https' : 'http';
const devServerUrl = url.format({protocol, hostname, port}); // *** 2 ***
/* ... */
console.info(`Starting dev server on ${devServerUrl}\n`); // *** 3 ***
/* ... */
await Metro.runServer(metroConfig, {
host: args.host, // *** 4 ***
Relevant parts of runServer function from runServer.js
We’ll assume the start command is called with the default values and no optional parameters. Let’s follow each of the steps in the code sample above:
-
hostnameholds thehostparameter, if supplied, or"localhost".devServerUrlis formed using hostname.- The misleading message
“Starting dev server on http://localhost:8081”is printed. - Metro.runServer function, responsible for running the Metro development server is called with the host being
args.host, which is undefined, instead of using thehostnamevariable.
Later on, Metro’s runServer’s code uses this undefined parameter when calling httpServer.listen
From node http’s server.listen documentation, we can understand that this function behaves like server.listen from net.Server, which states:
“If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available, or the unspecified IPv4 address (0.0.0.0) otherwise. In most operating systems, listening to the unspecified IPv6 address (::) may cause the net.Server to also listen on the unspecified IPv4 address (0.0.0.0).”
Explicitly passing undefined is functionally equivalent to omitting the parameter. Thus, the server by default will listen for external connections.
As a result – the development-only HTTP endpoints are exposed to network attackers.
Summary
This vulnerability shows that even straightforward Remote Code Execution flaws, such as passing user input to the system shell, are still found in real-world software, especially in cases where the dangerous sink function actually resides in 3rd-party code, which was the imported “open” function in this case. It’s a reminder that secure coding practices and automated security scanning are essential for preventing these easily exploitable flaws before they make it to production.
A good way to avoid these and similar vulnerabilities in your own code is by deploying JFrog Advanced Security’s SAST scanning which enables developers to identify and fix security issues early in the development process. With zero configuration required, JFrog SAST integrates seamlessly into existing workflows through IDE plugins and CLI tools, giving developers easy access to actionable findings without interrupting their flow.
For example, in the screenshot below we can see how JFrog SAST discovers the above vulnerability, when scanned as 1st party code directly from within Visual Studio Code:

JFrog SAST highlights the vulnerability in the code along with its details and provides Data Trace Evidence that shows the data flow from the user-input req.body field until it is used as an input to the dangerous open function.
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.





