Medium firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionOther issue in the DOM: Copy & Paste and Drag & Drop component
ComponentWidget
Bug ClassLogic Error
Tracker2038868
Fix commit4a3476df14cb (firefox) +75/-8
CISA KEVNot listed
CreditedFarhad Sajid Barbhuiya
Disclosed2026-07-21

Background

Mark-of-the-Web (MOTW)
a Windows security marking, stored in a file’s :Zone.Identifier alternate data stream, that records the file originated from an untrusted zone.
Alternate Data Stream (ADS)
an NTFS feature that attaches an extra named data stream (e.g. :Zone.Identifier) to a file, addressed via a trailing :streamname on the file name.
`FILEGROUPDESCRIPTOR` / `FILEDESCRIPTOR`
the Windows shell structures a data object advertises so that a drag-and-drop or paste target knows the virtual files (cItems count, each cFileName) it can fetch via IDataObject::GetData.
Internet shortcut (`.url`)
the virtual .url file nsDataObj synthesizes when a link is dragged or copied out of the browser to the filesystem.

Root Cause Analysis

When a link was dragged or pasted from Firefox to the Windows shell, nsDataObj::GetFileDescriptorInternetShortcutA/W built a FILEGROUPDESCRIPTOR describing exactly one virtual file — the .url shortcut — by allocating only sizeof(FILEGROUPDESCRIPTOR*) and setting cItems = 1. The security invariant that content saved from an untrusted web origin must carry Mark-of-the-Web was violated: because no :Zone.Identifier stream was advertised or produced, the dropped shortcut landed on disk with no Internet-zone marking.

The fix enlarges the allocation by one FILEDESCRIPTOR, appends a second entry named <title>:Zone.Identifier (guarding against MAX_PATH overflow), and sets cItems = 2, so the shell now requests two streams. GetFileContentsInternetShortcut was correspondingly extended to answer aFE.lindex == 1 with the ADS payload [ZoneTransfer]\r\nZoneId=3\r\n, which is the canonical Internet-zone (ZoneId=3) marker.

The result is that the saved shortcut inherits MOTW, restoring the downstream SmartScreen/warning behavior that trusts that marking.

Key insight
The core mistake was omitting the :Zone.Identifier ADS entirely when materializing a web-originated shortcut, so files crossed the trust boundary onto disk unmarked; the fix advertises and populates a second Zone.Identifier stream carrying ZoneId=3, re-establishing Mark-of-the-Web on the dropped file.

Attack Path

  1. Lure the user to drag or copy A malicious page presents a link (or shortcut-like element) that the victim drags or copies out to the desktop or a folder.
  2. Firefox emits an unmarked shortcut nsDataObj advertises a single-item FILEGROUPDESCRIPTOR with no :Zone.Identifier, so the shell writes the .url file without Mark-of-the-Web.
  3. Zone check is bypassed When the user later opens the .url, Windows finds no Internet-zone marking and skips the SmartScreen/“file from the Internet” trust prompts.
  4. Untrusted target launches with reduced friction The shortcut resolves to attacker-chosen content that would normally have triggered a security warning.

Impact Assessment

An attacker who gets a victim to drag or paste a link out of Firefox onto the Windows filesystem obtains a shortcut file lacking Mark-of-the-Web, defeating the OS-level Internet-zone warnings that would otherwise gate opening it. This is a client-side, Windows-only trust-boundary weakening operating in the browser/shell interaction context; it requires local user interaction (an explicit drag-and-drop or copy-paste to disk) rather than a remote silent trigger. The gain is bypass of a defense-in-depth safety prompt, not direct code execution.

Changed Functions

FunctionChangeNotes
if
widget/windows/nsDataObj.cpp
modified

Files Changed

  • widget/windows/nsDataObj.cpp

Audit Directions

  • Cross-boundary file materialization
    Audit every path where browser-originated content is written to disk or handed to the shell (drag-and-drop, paste, downloads) to confirm each attaches :Zone.Identifier/MOTW.
  • Descriptor/content `cItems` consistency
    Verify that any FILEGROUPDESCRIPTOR whose cItems or lindex set changes has matching allocation size and a corresponding GetData/GetFileContents handler for every advertised index.
  • `MAX_PATH` string assembly
    Review name-suffix concatenations (memcpy/wmemcpy into fixed cFileName buffers) to ensure length checks precede the copy and failure is refused rather than truncated.
diff --git a/widget/windows/nsDataObj.cpp b/widget/windows/nsDataObj.cpp
index 1d1118c0f1f..e67a5db1cf4 100644
--- a/widget/windows/nsDataObj.cpp
+++ b/widget/windows/nsDataObj.cpp
@@ -1227,8 +1227,13 @@ nsDataObj ::GetFileDescriptorInternetShortcutA(FORMATETC& aFE,
   nsAutoString title;
   if (NS_FAILED(ExtractShortcutTitle(title))) return E_OUTOFMEMORY;
 
+  // Allocate space for two FILEDESCRIPTOR entries: the .url file plus a
+  // ":Zone.Identifier" ADS so the dropped shortcut is marked Internet-zone
+  // (untrusted).
+  size_t const allocSize =
+      sizeof(FILEGROUPDESCRIPTORA) + sizeof(FILEDESCRIPTORA);
   HGLOBAL fileGroupDescHandle =
-      ::GlobalAlloc(GMEM_ZEROINIT | GMEM_SHARE, sizeof(FILEGROUPDESCRIPTORA));
+      ::GlobalAlloc(GMEM_ZEROINIT | GMEM_SHARE, allocSize);
   if (!fileGroupDescHandle) return E_OUTOFMEMORY;
 
   LPFILEGROUPDESCRIPTORA fileGroupDescA =
@@ -1249,11 +1254,24 @@ nsDataObj ::GetFileDescriptorInternetShortcutA(FORMATETC& aFE,
       strcpy(fileGroupDescA->fgd[0].cFileName, "Untitled.url");
     }
   }
-
-  // one file in the file block
-  fileGroupDescA->cItems = 1;
   fileGroupDescA->fgd[0].dwFlags = FD_LINKUI;
 
+  // Build the ":Zone.Identifier" ADS entry.
+  // If appending the suffix would overflow, refuse the entire descriptor.
+  constexpr char kAdsSuffix[] = ":Zone.Identifier";
+  constexpr size_t kAdsSuffixSize = sizeof(kAdsSuffix);  // includes terminator
+  size_t const mainLen = strnlen(fileGroupDescA->fgd[0].cFileName, MAX_PATH);
+  if (mainLen + kAdsSuffixSize > MAX_PATH) {
+    ::GlobalUnlock(fileGroupDescHandle);
+    ::GlobalFree(fileGroupDescHandle);
+    return HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND);
+  }
+  memcpy(fileGroupDescA->fgd[1].cFileName, fileGroupDescA->fgd[0].cFileName,
+         mainLen);
+  memcpy(fileGroupDescA->fgd[1].cFileName + mainLen, kAdsSuffix,
+         kAdsSuffixSize);
+  fileGroupDescA->cItems = 2;
+
   ::GlobalUnlock(fileGroupDescHandle);
   aSTG.hGlobal = fileGroupDescHandle;
   aSTG.tymed = TYMED_HGLOBAL;
@@ -1268,8 +1286,13 @@ nsDataObj ::GetFileDescriptorInternetShortcutW(FORMATETC& aFE,
   nsAutoString title;
   if (NS_FAILED(ExtractShortcutTitle(title))) return E_OUTOFMEMORY;
 
+  // Allocate space for two FILEDESCRIPTOR entries: the .url file plus a
+  // ":Zone.Identifier" ADS so the dropped shortcut is marked Internet-zone
+  // (untrusted).
+  size_t const allocSize =
+      sizeof(FILEGROUPDESCRIPTORW) + sizeof(FILEDESCRIPTORW);
   HGLOBAL fileGroupDescHandle =
-      ::GlobalAlloc(GMEM_ZEROINIT | GMEM_SHARE, sizeof(FILEGROUPDESCRIPTORW));
+      ::GlobalAlloc(GMEM_ZEROINIT | GMEM_SHARE, allocSize);
   if (!fileGroupDescHandle) return E_OUTOFMEMORY;
 
   LPFILEGROUPDESCRIPTORW fileGroupDescW =
@@ -1290,11 +1313,25 @@ nsDataObj ::GetFileDescriptorInternetShortcutW(FORMATETC& aFE,
       wcscpy(fileGroupDescW->fgd[0].cFileName, L"Untitled.url");
     }
   }
-
-  // one file in the file block
-  fileGroupDescW->cItems = 1;
   fileGroupDescW->fgd[0].dwFlags = FD_LINKUI;
 
+  // Build the ":Zone.Identifier" ADS entry.
+  // If appending the suffix would overflow, refuse the entire descriptor.
+  constexpr WCHAR kAdsSuffix[] = L":Zone.Identifier";
+  constexpr size_t kAdsSuffixLen =
+      (sizeof(kAdsSuffix) / sizeof(WCHAR));  // includes terminator
+  size_t const mainLen = wcsnlen(fileGroupDescW->fgd[0].cFileName, MAX_PATH);
+  if (mainLen + kAdsSuffixLen > MAX_PATH) {
+    ::GlobalUnlock(fileGroupDescHandle);
+    ::GlobalFree(fileGroupDescHandle);
+    return HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND);
+  }
+  wmemcpy(fileGroupDescW->fgd[1].cFileName, fileGroupDescW->fgd[0].cFileName,
+          mainLen);
+  wmemcpy(fileGroupDescW->fgd[1].cFileName + mainLen, kAdsSuffix,
+          kAdsSuffixLen);
+  fileGroupDescW->cItems = 2;
+
   ::GlobalUnlock(fileGroupDescHandle);
   aSTG.hGlobal = fileGroupDescHandle;
   aSTG.tymed = TYMED_HGLOBAL;
@@ -1310,6 +1347,36 @@ nsDataObj ::GetFileDescriptorInternetShortcutW(FORMATETC& aFE,
 //
 HRESULT
 nsDataObj ::GetFileContentsInternetShortcut(FORMATETC& aFE, STGMEDIUM& aSTG) {
+  // The descriptor advertises two entries: the .url content (lindex 0) and
+  // the ":Zone.Identifier" ADS that marks it as Internet-zone (lindex 1).
+  if (aFE.lindex == 1) {
+    constexpr char kZoneIdContent[] = "[ZoneTransfer]\r\nZoneId=3\r\n";
+    constexpr size_t kZoneIdLen = sizeof(kZoneIdContent) - 1;
+
+    nsAutoGlobalMem globalMem(nsHGLOBAL(::GlobalAlloc(GMEM_SHARE, kZoneIdLen)));
+    if (!globalMem) {
+      return E_OUTOFMEMORY;
+    }
+    char* contents = reinterpret_cast<char*>(::GlobalLock(globalMem.get()));
+    if (!contents) {
+      return E_OUTOFMEMORY;
+    }
+    memcpy(contents, kZoneIdContent, kZoneIdLen);
+    ::GlobalUnlock(globalMem.get());
+
+    if (aFE.tymed & TYMED_ISTREAM) {
+      RefPtr<IStream> stream = new CMemStream(
+          globalMem.disown(), kZoneIdLen, already_AddRefed<AutoCloseEvent>());
+      stream.forget(&aSTG.pstm);
+      aSTG.tymed = TYMED_ISTREAM;
+    } else {
+      aSTG.hGlobal = globalMem.disown();
+      aSTG.tymed = TYMED_HGLOBAL;
+    }
+    return S_OK;
+  }
+
+  MOZ_ASSERT(aFE.lindex == 0);
   static const char* kShellIconPref = "browser.shell.shortcutFavicons";
   nsAutoString url;
   if (NS_FAILED(ExtractShortcutURL(url))) return E_OUTOFMEMORY;
Loading diff…