I’ve been writing about the Mark-of-the-Web (MotW) security primitive in Windows for decades now, with 2016’s Downloads and MoTW being one of my longer posts that I’ve updated intermittently over the last few years. If you haven’t read that post already, you should start there.
Advice for Implementers
At this point, MotW is old enough to vote and almost old enough to drink, yet understanding of the feature remains patchy across the Windows developer ecosystem.
MotW, like most security primitives (e.g. HTTPS) only works if you use it. Specifically, an application which generates local files from untrusted data (i.e. anywhere on “The Internet”) must ensure that the files bear a MoTW to ensure that the Windows Shell and other applications recognize the files’ origins and treat them with appropriate caution. Such treatment might include running anti-malware checks, prompting the user before running unsafe executables, or opening the files in Office’s Protected View.
Similarly, if you build an application which consumes files, you should carefully consider whether files from untrusted origins should be treated with extra caution in the same way that Microsoft’s key applications behave — locking down or prompting users for permission before the file initiates any potentially-unwanted actions, more-tightly sandboxing parsers, etc.
Writing MotW
The best way to write a Mark-of-the-Web to a file is to let Windows do it for you, using the IAttachmentExecute::Save()
API. Using the Attachment Execution Services API ensures that the MotW is written (or not) based on the client’s configuration. Using the API also provides future-proofing for changes to the MotW format (e.g. Win10 started preserving the original URL information rather than just the ZoneID).
If the URL is not known, but you wish to ensure Internet Zone handling, use the special url about:internet
.
You should also use about:internet
if the URL is longer than 2083 characters (INTERNET_MAX_URL_LENGTH
), or if the URL’s scheme isn’t one of HTTP
/HTTPS
/FILE
.
Take care with anything that would prevent proper writing of the MotW– for example, if you build a decompression utility for ZIP files, ensure that you write the MotW before your utility applies any readonly
bit to the newly extracted file, otherwise the tagging will fail.
In certain (rare) scenarios, there’s the risk of a race condition whereby a client could consume a file before your code has had the chance to tag it with the Mark-of-the-Web, resulting in a security vulnerability. For instance, consider the case where your app (1) downloads a file from the internet, (2) streams the bytes to disk, (3) closes the file, finally (4) calls IAttachmentExecute::Save()
to let the system tag the file with the MotW. If an attacker can induce the handler for the new file to load it between steps #3 and #4, the file could be loaded before the MotW is applied. Unfortunately, there’s not generally a great way to prevent this — for example, the Save()
call can perform operations that depend on the file’s name and content (e.g. an antivirus scan) so we can’t simply call the API against an empty file or against a bogus temporary filename (i.e. inprogress.temp
). The best approach I can think of is to avoid exposing the file in a predictable location until the MotW marking is complete. For example, you could download the file into a randomly-named temporary folder (e.g. %TEMP%\InProgress\{guid}\setup.exe
), call the Save()
method on that file, then move the file to the predictable location.
Respecting MotW
To check the Zone for a URL, use the MapUrlToZone function in URLMon.dll. Because the MotW is typically stored as a simple key-value pair within a NTFS alternate data stream:
…it’s tempting to think “My code can just read the ZoneId
directly.”
Unfortunately, doing so is a recipe for failure.
Firstly, consider the simple corner cases you might miss. For instance, if you try to open with read/write permissions the Zone.Identifier
stream of a file whose readonly bit is set, the attempt to open the stream will fail because the file isn’t writable.
Second, there’s a ton of subtlety in performing a proper zone mapping.
2a: For example, files stored under certain paths or with certain Integrity Levels are treated as Internet Zone, even without a Zone.Identifier
stream:
2b: Similarly, files accessed via a \\UNC
share are implicitly not in the Local Machine Zone, even if they don’t have a Zone.Identifier
stream.
2c: As of the latest Windows 11 updates, if you zone-map a file contained within a virtual disk (e.g. a .iso
file), that file will inherit the MotW of the containing .iso
file, even though the embedded file has no Zone.Identifier
stream.
2d: For HTML files, a special saved from url comment allows specification of the original url of the HTML content. When MapUrlToZone is called on a HTML file URL, the start of the file is scanned for this comment, and if found, the stored URL is used for Zone Mapping:
Finally, the contents of the Zone.Identifier
stream are subject to change in the future. New key/value fields were added in Windows 10, and the format could be changed again in the future.
Comparing Zone Ids
In most cases, you’ll want to use < and > comparisons rather than exact Zone comparisons; for example, when treating content as “trustworthy”, you’ll typically want to check Zone<
3, and when deeming content risky, you’ll check Zone>3
.
Tool: Simple MapUrlToZone caller
Compile from a Visual Studio command prompt using csc mutz.cs
:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
using System; | |
using System.IO; | |
using System.Runtime.InteropServices; | |
namespace MUTZ | |
{ | |
[ComImport, GuidAttribute("79EAC9EE-BAF9-11CE-8C82-00AA004BA90B")] | |
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)] | |
public interface IInternetSecurityManager | |
{ | |
[return: MarshalAs(UnmanagedType.I4)][PreserveSig] | |
int SetSecuritySite([In] IntPtr pSite); | |
[return: MarshalAs(UnmanagedType.I4)][PreserveSig] | |
int GetSecuritySite([Out] IntPtr pSite); | |
[return: MarshalAs(UnmanagedType.I4)][PreserveSig] | |
int MapUrlToZone([In,MarshalAs(UnmanagedType.LPWStr)] string pwszUrl, | |
ref UInt32 pdwZone, UInt32 dwFlags); | |
[return: MarshalAs(UnmanagedType.I4)][PreserveSig] | |
int GetSecurityId([MarshalAs(UnmanagedType.LPWStr)] string pwszUrl, | |
[MarshalAs(UnmanagedType.LPArray)] byte[] pbSecurityId, | |
ref UInt32 pcbSecurityId, uint dwReserved); | |
[return: MarshalAs(UnmanagedType.I4)][PreserveSig] | |
int ProcessUrlAction([In,MarshalAs(UnmanagedType.LPWStr)] string pwszUrl, | |
UInt32 dwAction, out byte pPolicy, UInt32 cbPolicy, | |
byte pContext, UInt32 cbContext, UInt32 dwFlags, | |
UInt32 dwReserved); | |
[return: MarshalAs(UnmanagedType.I4)][PreserveSig] | |
int QueryCustomPolicy([In,MarshalAs(UnmanagedType.LPWStr)] string pwszUrl, | |
ref Guid guidKey, ref byte ppPolicy, ref UInt32 pcbPolicy, | |
ref byte pContext, UInt32 cbContext, UInt32 dwReserved); | |
[return: MarshalAs(UnmanagedType.I4)][PreserveSig] | |
int SetZoneMapping(UInt32 dwZone, | |
[In,MarshalAs(UnmanagedType.LPWStr)] string lpszPattern, | |
UInt32 dwFlags); | |
[return: MarshalAs(UnmanagedType.I4)][PreserveSig] | |
int GetZoneMappings(UInt32 dwZone, out System.Runtime.InteropServices.ComTypes.IEnumString ppenumString, | |
UInt32 dwFlags); | |
} | |
public class MUTZ | |
{ | |
private readonly static Guid CLSID_SecurityManager = new Guid("7b8a2d94-0ac9-11d1-896c-00c04fb6bfc4"); | |
public static int Main(string[] args) | |
{ | |
UInt32 iZone=0; | |
string sURL = "https://example.com/"; | |
if (args.Length > 0) | |
{ | |
sURL = args[0]; | |
} | |
else | |
{ | |
Console.WriteLine("Usage: mutz.exe https://host/path?query#fragment\n\n"); | |
} | |
Type t = Type.GetTypeFromCLSID(CLSID_SecurityManager); | |
object securityManager = Activator.CreateInstance(t); | |
IInternetSecurityManager ISM = securityManager as IInternetSecurityManager; | |
ISM.MapUrlToZone(sURL, ref iZone, 0); // TODO: Allow specification of flags https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/dd759042(v=vs.85) | |
Marshal.ReleaseComObject(securityManager); | |
string sZone; | |
switch (iZone) | |
{ | |
case 0: sZone = "LocalMachine"; break; | |
case 1: sZone = "LocalIntranet"; break; | |
case 2: sZone = "Trusted"; break; | |
case 3: sZone = "Internet"; break; | |
case 4: sZone = "Restricted"; break; | |
default: sZone = "~custom~"; break; | |
} | |
Console.WriteLine($"URL: {sURL}"); | |
Console.WriteLine($"Zone: {iZone} ({sZone})"); | |
Uri uri; | |
if (Uri.TryCreate(sURL, UriKind.Absolute, out uri)) { | |
if (uri.IsFile) { | |
string strPath = uri.LocalPath; | |
Console.WriteLine($"Filesystem Path: {strPath}"); | |
Console.WriteLine($"IsUnc: {uri.IsUnc}"); | |
if (uri.IsUnc) { | |
// 0x00000400 – MUTZ require saved file check | |
} | |
/* | |
// It would be nice if this worked, but it doesn't because .NET Framework doesn't support opening the alternate stream. | |
// See https://stackoverflow.com/questions/604960/how-to-read-and-modify-ntfs-alternate-data-streams-using-net | |
try { | |
string strMotW = File.ReadAllText($"{strPath}:Zone.Identifier"); | |
Console.WriteLine(":ZoneIdentifier\n{strMotW}\n————————————-\n\n"); | |
} catch (Exception eX) { | |
Console.WriteLine($"ZoneIdentifier stream could not be read ({eX.Message})"); | |
} | |
*/ | |
} | |
} | |
return (int)iZone; | |
} | |
} | |
} |
Impatient optimist. Dad. Author/speaker. Created Fiddler & SlickRun. PM @ MSFT '01-'12, and '18-, working on Office, IE, Edge, and Web Protection. My words are my own, I do not speak for any other entity. View more posts