Medium firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionPrivilege escalation in the Shell Integration component
ComponentDOM
Bug ClassLogic Error
Tracker2053455
Fix commitb3ccdee1272d (firefox) +34/-12
CISA KEVNot listed
CreditedKhanh Nguyen
Disclosed2026-08-18

Background

Jump List
a Windows taskbar feature where an application registers shortcut entries that relaunch the executable with predefined command-line arguments.
`WindowsJumpListShortcutDescription`
the WebIDL dictionary chrome JavaScript uses to describe one Jump List entry, including the arguments passed to the executable.
`SetArguments`
the Windows IShellLink method that stores a raw command-line argument string on a shortcut, later concatenated onto the executable path at launch.
`MakeCommandLine`
a Gecko helper that assembles an argv array into a single properly quoted and escaped Windows command-line string.

Root Cause Analysis

The vulnerable path was JumpListBuilder::GetShellLinkFromDescription in widget/windows/JumpListBuilder.cpp, which took the caller-supplied aDesc.mArguments DOMString and handed it verbatim to psl->SetArguments. In browser/modules/WindowsJumpLists.sys.mjs, the frecency-based history builder set arguments directly to row.getResultByName("url"), so an attacker-influenced URL flowed unescaped into the shortcut’s command line. Because Windows re-parses that string into separate argv tokens at launch, a URL containing spaces and flag-like tokens violated the invariant that the URL is a single opaque argument, letting it inject additional command-line switches.

The fix redefines arguments as a sequence<DOMString> so each token stays distinct, and GetShellLinkFromDescription now runs the sequence through MakeCommandLine, which quotes and escapes every element before calling SetArguments. The history builder additionally wraps the URL as ["-osint", "-url", url], forcing it down the OS-integration launch path that treats the value strictly as a URL rather than as parseable command-line input.

Key insight
The single mistake was treating a structured argv list as one flat, unescaped command-line string, so an attacker-controlled URL could smuggle extra arguments; the fix makes arguments a real sequence and reconstructs the command line with MakeCommandLine so each element is safely quoted.

Attack Path

  1. Poison history An attacker gets the victim to visit or record a crafted URL containing spaces and command-line-flag tokens so it becomes a frecent entry.
  2. Jump List generation Firefox’s Builder reads the URL and stores it as the raw arguments string for a taskbar Jump List shortcut.
  3. Unescaped assignment GetShellLinkFromDescription passes that string straight to SetArguments, embedding the injected tokens into the shortcut’s command line.
  4. Relaunch with injected switches The victim clicks the Jump List entry and Windows re-parses the command line, delivering the attacker’s extra flags to the Firefox process instead of a single URL argument.

Impact Assessment

An attacker gains the ability to inject additional command-line switches into a Firefox relaunch triggered from the Windows taskbar Jump List, corresponding to the reported privilege escalation in the Shell Integration component. This executes in the browser process at launch time in the victim’s Windows session, and requires the attacker-controlled URL to reach the frecent-history Jump List and the victim to click the crafted entry.

Changed Functions

FunctionChangeNotes
for
widget/windows/JumpListBuilder.cpp
modified
if
widget/windows/tests/gtest/TestJumpListBuilder.cpp
modified

Files Changed

  • browser/modules/WindowsJumpLists.sys.mjs
  • dom/chrome-webidl/WindowsJumpListShortcutDescription.webidl
  • widget/windows/JumpListBuilder.cpp
  • widget/windows/tests/gtest/TestJumpListBuilder.cpp

Audit Directions

  • String-as-argv
    Flag any place a single string is passed to SetArguments, ShellExecute, or process-launch APIs where it originates from web-influenced data and is later re-tokenized.
  • OSINT bypass
    Verify that all externally sourced URLs handed to browser relaunch paths go through -osint/-url rather than being placed as bare command-line values.
  • WebIDL type widening
    Review other chrome WebIDL dictionaries where a DOMString conflates multiple logical fields (arguments, paths) and should be a sequence<DOMString> with per-element escaping.
diff --git a/browser/modules/WindowsJumpLists.sys.mjs b/browser/modules/WindowsJumpLists.sys.mjs
index 18d6fb85be4..5ddca6c3776 100644
--- a/browser/modules/WindowsJumpLists.sys.mjs
+++ b/browser/modules/WindowsJumpLists.sys.mjs
@@ -93,7 +93,7 @@ var tasksCfg = [
     get description() {
       return _getString("taskbar.tasks.newTab.description");
     },
-    args: "-new-tab about:blank",
+    args: ["-new-tab", "about:blank"],
     iconIndex: 3, // New window icon
     open: true,
     close: true, // The jump list already has an app launch icon, but
@@ -109,7 +109,7 @@ var tasksCfg = [
     get description() {
       return _getString("taskbar.tasks.newWindow.description");
     },
-    args: "-browser",
+    args: ["-browser"],
     iconIndex: 2, // New tab icon
     open: true,
     close: true, // No point, but we don't always update the list on
@@ -125,7 +125,7 @@ let privateWindowTask = {
   get description() {
     return _getString("taskbar.tasks.newPrivateWindow.description");
   },
-  args: "-private-window",
+  args: ["-private-window"],
   iconIndex: 4, // Private browsing mode icon
   open: true,
   close: true, // No point, but we don't always update the list on
@@ -267,7 +267,7 @@ var Builder = class {
             title: row.getResultByName("title"),
             description: row.getResultByName("title"),
             path: selfPath,
-            arguments: row.getResultByName("url"),
+            arguments: ["-osint", "-url", row.getResultByName("url")],
             fallbackIconIndex: 1,
             iconPath,
           });
diff --git a/dom/chrome-webidl/WindowsJumpListShortcutDescription.webidl b/dom/chrome-webidl/WindowsJumpListShortcutDescription.webidl
index d32eb6c61be..73767c84ac6 100644
--- a/dom/chrome-webidl/WindowsJumpListShortcutDescription.webidl
+++ b/dom/chrome-webidl/WindowsJumpListShortcutDescription.webidl
@@ -27,7 +27,7 @@ dictionary WindowsJumpListShortcutDescription {
    * Arguments to be supplied to the executable when the item is selected in
    * the Jump List.
    */
-  DOMString arguments;
+  sequence<DOMString> arguments;
 
   /**
    * A description of the item that is displayed as a tooltip.
diff --git a/widget/windows/JumpListBuilder.cpp b/widget/windows/JumpListBuilder.cpp
index 02e5f7a0d3b..121e69236c5 100644
--- a/widget/windows/JumpListBuilder.cpp
+++ b/widget/windows/JumpListBuilder.cpp
@@ -13,6 +13,7 @@
 // clang-format on
 
 #include "WinUtils.h"
+#include "mozilla/CmdLineAndEnvUtils.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/WindowsJumpListShortcutDescriptionBinding.h"
@@ -876,10 +877,20 @@ nsresult JumpListBuilder::GetShellLinkFromDescription(
 
   hr = psl->SetDescription(descriptionCopy.get());
 
-  if (aDesc.mArguments.WasPassed() && !aDesc.mArguments.Value().IsEmpty()) {
-    hr = psl->SetArguments(aDesc.mArguments.Value().get());
-  } else {
-    hr = psl->SetArguments(L"");
+  if (aDesc.mArguments.WasPassed()) {
+    const mozilla::dom::Sequence<nsString>& arguments =
+        aDesc.mArguments.Value();
+
+    // MakeCommandLine expects wchar_t**, but we have Sequence<nsString>, so
+    // convert it over.
+    AutoTArray<const wchar_t*, 8> flatArgs;
+    for (const nsString& arg : arguments) {
+      flatArgs.AppendElement(arg.get());
+    }
+
+    UniquePtr<wchar_t[]> commandLine = mozilla::MakeCommandLine(
+        AssertedCast<int>(flatArgs.Length()), flatArgs.Elements());
+    hr = psl->SetArguments(commandLine.get());
   }
 
   // Set up the fallback icon in the event that a valid icon URI has
diff --git a/widget/windows/tests/gtest/TestJumpListBuilder.cpp b/widget/windows/tests/gtest/TestJumpListBuilder.cpp
index a46eb4e5051..d6dd8208fce 100644
--- a/widget/windows/tests/gtest/TestJumpListBuilder.cpp
+++ b/widget/windows/tests/gtest/TestJumpListBuilder.cpp
@@ -17,6 +17,7 @@ PSSTDAPI PropVariantToString(REFPROPVARIANT propvar, PWSTR psz, UINT cch);
 #include "JumpListBuilder.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
+#include "mozilla/CmdLineAndEnvUtils.h"
 #include "mozilla/SpinEventLoopUntil.h"
 #include "mozilla/dom/BindingDeclarations.h"
 #include "mozilla/dom/Promise.h"
@@ -113,7 +114,17 @@ MATCHER_P(ShellLinksEq, descs,
     }
 
     if (desc.mArguments.WasPassed()) {
-      if (!desc.mArguments.Value().Equals(argsBuf)) {
+      mozilla::CommandLineParserWin<char16_t> parser;
+      parser.HandleCommandLine(nsDependentString(argsBuf));
+
+      const mozilla::dom::Sequence<nsString>& args = desc.mArguments.Value();
+
+      bool same = std::equal(
+          parser.Argv(), parser.Argv() + parser.Argc(), args.cbegin(),
+          args.cend(), [](const char16_t* aParsed, const nsAString& aExpected) {
+            return aExpected == nsDependentString(aParsed);
+          });
+      if (!same) {
         return false;
       }
     } else {
@@ -310,8 +321,8 @@ void GenerateWindowsJumpListShortcutDescriptions(
     desc.mFallbackIconIndex = 0;
 
     if (!(i % 2)) {
-      nsAutoString arguments(u"-arg1 -arg2 -arg3");
-      desc.mArguments.Construct(arguments);
+      desc.mArguments.Construct(nsTArray<nsString>{
+          u"-arg1"_ns, u"argument with a space"_ns, u"-arg3"_ns});
       nsAutoString iconPath(u"C:\\Some\\icon.png");
       desc.mIconPath.Construct(iconPath);
     }
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/widget/windows/tests/gtest/TestJumpListBuilder.cpp b/widget/windows/tests/gtest/TestJumpListBuilder.cpp
index a46eb4e5051..d6dd8208fce 100644
--- a/widget/windows/tests/gtest/TestJumpListBuilder.cpp
+++ b/widget/windows/tests/gtest/TestJumpListBuilder.cpp
@@ -17,6 +17,7 @@ PSSTDAPI PropVariantToString(REFPROPVARIANT propvar, PWSTR psz, UINT cch);
 #include "JumpListBuilder.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
+#include "mozilla/CmdLineAndEnvUtils.h"
 #include "mozilla/SpinEventLoopUntil.h"
 #include "mozilla/dom/BindingDeclarations.h"
 #include "mozilla/dom/Promise.h"
@@ -113,7 +114,17 @@ MATCHER_P(ShellLinksEq, descs,
     }
 
     if (desc.mArguments.WasPassed()) {
-      if (!desc.mArguments.Value().Equals(argsBuf)) {
+      mozilla::CommandLineParserWin<char16_t> parser;
+      parser.HandleCommandLine(nsDependentString(argsBuf));
+
+      const mozilla::dom::Sequence<nsString>& args = desc.mArguments.Value();
+
+      bool same = std::equal(
+          parser.Argv(), parser.Argv() + parser.Argc(), args.cbegin(),
+          args.cend(), [](const char16_t* aParsed, const nsAString& aExpected) {
+            return aExpected == nsDependentString(aParsed);
+          });
+      if (!same) {
         return false;
       }
     } else {
@@ -310,8 +321,8 @@ void GenerateWindowsJumpListShortcutDescriptions(
     desc.mFallbackIconIndex = 0;
 
     if (!(i % 2)) {
-      nsAutoString arguments(u"-arg1 -arg2 -arg3");
-      desc.mArguments.Construct(arguments);
+      desc.mArguments.Construct(nsTArray<nsString>{
+          u"-arg1"_ns, u"argument with a space"_ns, u"-arg3"_ns});
       nsAutoString iconPath(u"C:\\Some\\icon.png");
       desc.mIconPath.Construct(iconPath);
     }
Loading diff…