Medium CVSS 4.8 firefox Input Validation 🔧 Commit mapped

Overview

Medium
Severity
4.8
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionDue to insufficient escaping of the ampersand character in the “Copy as cURL” feature, an attacker could trick a user into using this command, potentially leading to local code execution on the user's system.<br>*This bug only affects Firefox for Windows. Other versions of Firefox are unaffected.*
ComponentCore
Bug ClassInput Validation
Tracker1962301
Fix commitc035eef75934 (firefox) +12/-2
CWECWE-77
CVSS vectorCVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:L/I:L/A:L
CISA KEVNot listed
CreditedAmeen Basha M K
Disclosed2025-05-27

Background

"Copy as cURL"
a DevTools Network-monitor feature that serializes a captured HTTP request into a curl command line the user can paste into a shell. The serialization lives in devtools/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.exe treats & (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.exe the 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.

Key insight
The ampersand/pipe escaping was applied only to the ANSI-C $'...' 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

  1. Attacker (web content) serves a page or endpoint whose request/response includes a field (header, URL, POST body) containing &calc.exe or similar Windows command fragments.
  2. Victim (analyst) opens DevTools Network monitor, right-clicks the request, and chooses “Copy as cURL” (user interaction required; UI:R in the CVSS vector).
  3. Serialization escapeStringPosix takes the plain single-quote path and emits the &/| verbatim inside '...'.
  4. Paste into cmd.exe the victim pastes the command into a Windows command prompt; cmd.exe ignores the single quotes and treats &calc.exe as a chained command.
  5. 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

This is a content-injection/command-injection issue affecting only Firefox for Windows (per the advisory), requiring user interaction (the victim must copy and then run the generated command in 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.js
  • devtools/client/shared/curl.js

Audit Directions

  • Other serializers/exporters
    review “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 metacharacters
    the fix only covers & and |; audit whether <, >, ^, % (variable expansion) and newline handling in cmd.exe/PowerShell are fully neutralized across all branches.
  • ANSI-C vs single-quote branch parity
    ensure any future transformation added to one branch of escapeStringPosix is mirrored in the other, since the two diverged here.
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() {
Loading diff…

Regression Test / PoC

shipped with the fix
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() {
Loading diff…