CVE-2026-74965
Overview
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
argumentspassed to the executable. - `SetArguments`
- the Windows
IShellLinkmethod 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.
MakeCommandLine so each element is safely quoted.Attack Path
- 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.
- Jump List generation
Firefox’s
Builderreads the URL and stores it as the rawargumentsstring for a taskbar Jump List shortcut. - Unescaped assignment
GetShellLinkFromDescriptionpasses that string straight toSetArguments, embedding the injected tokens into the shortcut’s command line. - 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
forwidget/windows/JumpListBuilder.cpp |
modified | |
ifwidget/windows/tests/gtest/TestJumpListBuilder.cpp |
modified |
Files Changed
browser/modules/WindowsJumpLists.sys.mjsdom/chrome-webidl/WindowsJumpListShortcutDescription.webidlwidget/windows/JumpListBuilder.cppwidget/windows/tests/gtest/TestJumpListBuilder.cpp
Audit Directions
- String-as-argvFlag 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 bypassVerify that all externally sourced URLs handed to browser relaunch paths go through
-osint/-urlrather than being placed as bare command-line values. - WebIDL type wideningReview other chrome WebIDL dictionaries where a
DOMStringconflates multiple logical fields (arguments, paths) and should be asequence<DOMString>with per-element escaping.
Patch
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);
}
Regression Test / PoC
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);
}