CVE-2026-16378
Overview
Background
- Mark-of-the-Web (MOTW)
- a Windows security marking, stored in a file’s
:Zone.Identifieralternate 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:streamnameon 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 (
cItemscount, eachcFileName) it can fetch viaIDataObject::GetData. - Internet shortcut (`.url`)
- the virtual
.urlfilensDataObjsynthesizes 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.
: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
- 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.
- Firefox emits an unmarked shortcut
nsDataObjadvertises a single-itemFILEGROUPDESCRIPTORwith no:Zone.Identifier, so the shell writes the.urlfile without Mark-of-the-Web. - 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. - Untrusted target launches with reduced friction The shortcut resolves to attacker-chosen content that would normally have triggered a security warning.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifwidget/windows/nsDataObj.cpp |
modified |
Files Changed
widget/windows/nsDataObj.cpp
Audit Directions
- Cross-boundary file materializationAudit 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` consistencyVerify that any
FILEGROUPDESCRIPTORwhosecItemsorlindexset changes has matching allocation size and a correspondingGetData/GetFileContentshandler for every advertised index. - `MAX_PATH` string assemblyReview name-suffix concatenations (
memcpy/wmemcpyinto fixedcFileNamebuffers) to ensure length checks precede the copy and failure is refused rather than truncated.
Patch
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;