CVE-2025-4089
Overview
Background
- "Copy as cURL"
- A DevTools Network Monitor feature that serializes a captured request into a shell
curlcommand the user can paste into a terminal. - `cmd.exe` escape semantics
- In
cmd.exe,&is a command separator (a & bruns both).^is the escape character, so^&is a literal ampersand; the pre-patch escaper never neutralized&. - Command injection (CWE-77)
- If request data containing
&is pasted intocmd.exe, everything after the&is executed as a separate command.
Root Cause Analysis
escapeStringWin neutralized a set of shell-dangerous characters (", \, `, $, %, newlines, non-printables) but omitted the cmd.exe command separator &. Attacker-influenced request fields (URL, headers, POST body) flow into the generated curl string through this escaper, so a value such as foo & calc.exe was emitted with the & intact. When the victim copied the “Copy as cURL” output and ran it in a Windows command prompt, cmd.exe interpreted the unescaped & as a statement separator and executed the trailing text as a second command.
The patch adds one replacement to the escapeStringWin pipeline, .replace(/&/g, "^&"), inserted before the final non-printable escape. This prefixes every & with the cmd.exe escape caret so it is treated as a literal ampersand rather than a command separator. The added test testEscapeStringPosix/Win case confirms that cmd & calc.exe becomes cmd ^& calc.exe, neutralizing the injection. The advisory labels this CWE-77/LogicError; the diff substantiates it concretely as improper neutralization of a shell metacharacter (&) — an input-neutralization/injection defect specific to the Windows shell.
& is a cmd.exe command separator, so attacker-controlled request data carrying & could chain an arbitrary second command into the copied-and-run curl line.Attack Path
- Attacker
Induces the victim to make (or inspect) an HTTP request whose data the attacker controls — e.g. a URL, header, or POST body containing
...& calc.exe ...(or a more harmful payload). - Victim Opens DevTools Network Monitor and uses “Copy as cURL” on that request (the social-engineering step; UI:N in the vector reflects it does not require interaction from the attacker’s page, but the user must run the command).
- Serialization
escapeStringWinemits the attacker’s value with&unescaped. - Paste & run
The victim pastes the command into a Windows
cmd.exeprompt. - Execution
cmd.exesplits on the unescaped&and runs the attacker’s trailing text as a local command — local code execution in the victim’s user context.
Impact Assessment
cmd.exe. No browser process memory is corrupted; the “sink” is the user’s shell. Gating is significant (victim must use the feature and paste into a Windows prompt), consistent with the moderate rating, but the outcome when it fires is full local command execution. The POSIX path is unaffected by this specific & gap.Files Changed
devtools/client/netmonitor/test/browser_net_curl-utils.jsdevtools/client/shared/curl.js
Audit Directions
- Other `cmd.exe` metacharactersVerify the Windows escaper also neutralizes the remaining shell-significant characters (
|,<,>,(,),^, delayed-expansion!); the follow-up CVE-2025-5264 reworked this escaper further, so cross-check both. - Other "copy as" / shell serializersAny DevTools or toolchain feature that emits shell-runnable text from captured network data (PowerShell, fetch, wget exporters) should be audited for the same unescaped-separator class.
- Escaper test coverageEnsure the test matrix includes every shell separator for both POSIX and Windows targets, not just the ampersand added here.
Patch
diff --git a/devtools/client/shared/curl.js b/devtools/client/shared/curl.js
index 47d2aacfe81..0af5a608cb2 100644
--- a/devtools/client/shared/curl.js
+++ b/devtools/client/shared/curl.js
@@ -424,6 +424,7 @@ const CurlUtils = {
.replace(/\n/g, "\\n")
.replace(/\r/g, "\\r")
.replace(/!/g, "\\041")
+ .replace(/&/g, "^&")
.replace(/[^\x20-\x7E]/g, escapeCharacter) +
"'"
);
diff --git a/devtools/client/netmonitor/test/browser_net_curl-utils.js b/devtools/client/netmonitor/test/browser_net_curl-utils.js
index cdf64ad5f95..17452405eda 100644
--- a/devtools/client/netmonitor/test/browser_net_curl-utils.js
+++ b/devtools/client/netmonitor/test/browser_net_curl-utils.js
@@ -295,6 +295,14 @@ function testEscapeStringPosix() {
"$'\\xc3\\xa6 \\xc3\\xb8 \\xc3\\xbc \\xc3\\x9f \\xc3\\xb6 \\xc3\\xa9'",
"Character codes outside of the decimal range 32 - 126 should be escaped."
);
+
+ // Assert that ampersands are correctly escaped in case its tried to run on Windows
+ const evilCommand = `query=evil\n\ncmd & calc.exe\n\n`;
+ is(
+ CurlUtils.escapeStringPosix(evilCommand),
+ "$'query=evil\\n\\ncmd ^& calc.exe\\n\\n'",
+ "The evil command is escaped properly"
+ );
}
function testEscapeStringWin() {
Regression Test / PoC
diff --git a/devtools/client/netmonitor/test/browser_net_curl-utils.js b/devtools/client/netmonitor/test/browser_net_curl-utils.js
index cdf64ad5f95..17452405eda 100644
--- a/devtools/client/netmonitor/test/browser_net_curl-utils.js
+++ b/devtools/client/netmonitor/test/browser_net_curl-utils.js
@@ -295,6 +295,14 @@ function testEscapeStringPosix() {
"$'\\xc3\\xa6 \\xc3\\xb8 \\xc3\\xbc \\xc3\\x9f \\xc3\\xb6 \\xc3\\xa9'",
"Character codes outside of the decimal range 32 - 126 should be escaped."
);
+
+ // Assert that ampersands are correctly escaped in case its tried to run on Windows
+ const evilCommand = `query=evil\n\ncmd & calc.exe\n\n`;
+ is(
+ CurlUtils.escapeStringPosix(evilCommand),
+ "$'query=evil\\n\\ncmd ^& calc.exe\\n\\n'",
+ "The evil command is escaped properly"
+ );
}
function testEscapeStringWin() {