CVE-2025-5265
Overview
Background
- "Copy as cURL"
- a DevTools Network-monitor feature that serializes a captured HTTP request into a
curlcommand line the user can paste into a shell. The serialization lives indevtools/client/shared/curl.js. - `CurlUtils.escapeStringPosix`
- the helper that quotes a string for a POSIX-style shell. It has two output paths: an ANSI-C
$'...'path (used when the string contains non-printable characters or a single quote) and a plain single-quote'...'path for everything else. - Windows shell metacharacters
cmd.exetreats&(command separator) and|(pipe) as special even though Firefox emits a POSIX-style quoted command; on Windows these characters can break out of the intended argument and start a second command.- `^` escaping
- on
cmd.exethe caret^escapes the following character, so^&is a literal ampersand rather than a command separator.
Root Cause Analysis
CurlUtils.escapeStringPosix already neutralized & and | in its ANSI-C quoting branch via .replace(/([&\|])/g, "^$1"), but the second, far more common branch — the plain single-quote path return "'" + str + "'"; — emitted the string verbatim. On Linux/macOS single quotes fully protect the content, so this was safe there. On Windows, however, cmd.exe does not honor the single quotes that Firefox wraps around the value, so any & or | embedded in an attacker-influenced request field (e.g. a header value such as EvilHeader: &calc.exe&) survives into the command line and is interpreted as a command separator/pipe. The fix hoists the Windows special-character regex into a shared winSpecialCharsRegEx = /([&\|])/g and, critically, applies it in the single-quote path too: return "'" + str.replace(winSpecialCharsRegEx, "^$1") + "'";. The added test asserts that "EvilHeader: &calc.exe&" now serializes to 'EvilHeader: ^&calc.exe^&'.
The underlying defect is insufficient output encoding (shell metacharacter escaping) on one of two code paths — an injection bug, not a memory-safety bug — matching the advisory’s CWE-77 (command injection). The advisory’s “Copy as cURL” framing is accurate; the diff substantiates it precisely.
$'...' quoting branch; the ordinary single-quote branch — which handles most request fields — left & and | unescaped, and because cmd.exe ignores single quotes, those characters became live command separators on Windows.Attack Path
- Attacker (web content)
serves a page or endpoint whose request/response includes a field (header, URL, POST body) containing
&calc.exeor similar Windows command fragments. - Victim (analyst) opens DevTools Network monitor, right-clicks the request, and chooses “Copy as cURL” (user interaction required; UI:R in the CVSS vector).
- Serialization
escapeStringPosixtakes the plain single-quote path and emits the&/|verbatim inside'...'. - Paste into cmd.exe
the victim pastes the command into a Windows command prompt;
cmd.exeignores the single quotes and treats&calc.exeas a chained command. - Primitive execution of attacker-chosen commands on the victim’s machine (local code execution), gated on the victim pasting the generated command on Windows.
Impact Assessment
cmd.exe). The gained primitive is local command execution in the victim’s shell context, but it is heavily gated: the attacker must lure the victim into using “Copy as cURL” and pasting the result into a Windows prompt. No memory corruption or sandbox escape is involved; the low CVSS (4.8) reflects the required interaction and local vector.Files Changed
devtools/client/netmonitor/test/browser_net_curl-utils.jsdevtools/client/shared/curl.js
Audit Directions
- Other serializers/exportersreview “Copy as PowerShell”, “Copy as fetch”, and HAR export paths in
devtools/client/shared/for the same asymmetry where one quoting branch escapes shell metacharacters and another does not. - Platform-specific metacharactersthe fix only covers
&and|; audit whether<,>,^,%(variable expansion) and newline handling incmd.exe/PowerShell are fully neutralized across all branches. - ANSI-C vs single-quote branch parityensure any future transformation added to one branch of
escapeStringPosixis mirrored in the other, since the two diverged here.
Patch
diff --git a/devtools/client/shared/curl.js b/devtools/client/shared/curl.js
index 5f30c411583..2d0f142fd44 100644
--- a/devtools/client/shared/curl.js
+++ b/devtools/client/shared/curl.js
@@ -421,6 +421,9 @@ const CurlUtils = {
return "\\u" + ("0000" + code).substr(code.length, 4);
}
+ // Escape & and |, which are special characters on Windows.
+ const winSpecialCharsRegEx = /([&\|])/g;
+
if (/[^\x20-\x7E]|\'/.test(str)) {
// Use ANSI-C quoting syntax.
return (
@@ -431,14 +434,14 @@ const CurlUtils = {
.replace(/\n/g, "\\n")
.replace(/\r/g, "\\r")
.replace(/!/g, "\\041")
- .replace(/([&\|])/g, "^$1")
+ .replace(winSpecialCharsRegEx, "^$1")
.replace(/[^\x20-\x7E]/g, escapeCharacter) +
"'"
);
}
// Use single quote syntax.
- return "'" + str + "'";
+ return "'" + str.replace(winSpecialCharsRegEx, "^$1") + "'";
},
/**
diff --git a/devtools/client/netmonitor/test/browser_net_curl-utils.js b/devtools/client/netmonitor/test/browser_net_curl-utils.js
index d4157c792cf..ef98d85b15c 100644
--- a/devtools/client/netmonitor/test/browser_net_curl-utils.js
+++ b/devtools/client/netmonitor/test/browser_net_curl-utils.js
@@ -303,6 +303,13 @@ function testEscapeStringPosix() {
"$'query=evil\\n\\ncmd ^& calc.exe\\n\\n'",
"The evil command is escaped properly"
);
+
+ const str = "EvilHeader: &calc.exe&";
+ is(
+ CurlUtils.escapeStringPosix(str),
+ "'EvilHeader: ^&calc.exe^&'",
+ "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 d4157c792cf..ef98d85b15c 100644
--- a/devtools/client/netmonitor/test/browser_net_curl-utils.js
+++ b/devtools/client/netmonitor/test/browser_net_curl-utils.js
@@ -303,6 +303,13 @@ function testEscapeStringPosix() {
"$'query=evil\\n\\ncmd ^& calc.exe\\n\\n'",
"The evil command is escaped properly"
);
+
+ const str = "EvilHeader: &calc.exe&";
+ is(
+ CurlUtils.escapeStringPosix(str),
+ "'EvilHeader: ^&calc.exe^&'",
+ "The evil command is escaped properly"
+ );
}
function testEscapeStringWin() {