Mark-of-the-Web: Additional Guidance

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.

Ensure that you write the MotW to any untrusted file written to disk, regardless of how that happened. For example, one mail client would properly write MotW when the user used the “Save” command on an attachment, but failed to do so if the user drag/dropped the attachment to their desktop. Similarly, browsers have written MotW to “downloads” for decades, but needed to add similar marking when the File Access API was introduced.

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.

MutZ Performance

One important consideration when calling MapUrlToZone() is that it is a blocking API which can take from milliseconds (common case) to tens of seconds (worst case) to complete. As such, you should NOT call this API on a UI thread– instead, call it from a background thread and asynchronously report the result up to the UI thread.

It’s natural to wonder how it’s possible that this API takes so long to complete in the worst case. While file system performance is unpredictable, even under load it rarely takes more than a few milliseconds, so checking the Zone.Identifier is not the root cause of slow performance. Instead, the worst performance comes when the system configuration enables the Local Intranet Zone, with the option to map to the Intranet Zone any site that bypasses the proxy server:

In this configuration, URLMon may need to discover a proxy configuration script (potentially taking seconds), download that script (potentially taking seconds), and run the FindProxyForURL function inside the script. That function may perform a number of expensive operations (including DNS resolutions), potentially taking seconds.

Fortunately, the “worst case” performance is not common after Windows 7 (the WinHTTP Proxy Service means that typically much of this work has already been done), but applications should still take care to avoid calling MapUrlToZone() on a UI thread, lest an annoyed user conclude that your application has hung and kill it.

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:

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;
}
}
}
view raw mutz.cs hosted with ❤ by GitHub

Q4 Races

I finished the first section of Tommy Rivers’ half-marathon training series (in Bolivia) and have moved on to the second section (Japan). I ran two Austin races in November, notching some real-world running experience in preparation for the 3M Half Marathon that I’ll be running at the end of January.

Run for the Water

On November 6th, I ran the “Run for the Water” ten miler, a charity race in support of providing clean water sources in Burundi.

Fortunately, everything that could’ve gone wrong with this race didn’t– the weather was nice, and my full belly had no complaints. This was my first race experience with music (my Amazon Fire phone to one Bluetooth headphone) and a carried snack (GU chews), and I figured out how to coax my watch into providing pacing information every half mile.

I had two goals for the race: To run the whole thing without stopping, and to beat 1:30 overall.

I achieved both, with a finishing time of 1:28:57, a pace of 8:53 per mile, and 1294 calories expended.

As predicted, I started at a faster pace before leveling out, with my slowest times in the hills around mile six:

The mid-race hills weren’t as bad as I feared, and I spent most of mile 6 and 7 psyching myself up for one final big hill that never arrived. By mile 8, I was daydreaming about blazing through miles 9 and 10, but started lagging and only sprinted at the very end. With an eye toward the half marathon, as I crossed the finish line, I asked myself whether I could run another 3.1 miles in thirty minutes and concluded “probably, but just barely.”

Notably, I managed to keep my heart rate under control for almost the whole race, running nearly the entire thing at just under 85% of my max:

The cool-but-not-cold weather undoubtably helped.

2023 Turkey Trot

On a drizzly Thanksgiving morning, I ran the Turkey Trot 5-miler and had another solid run, although I didn’t take it as seriously and I ended up missing both of my goals: Run the entire thing, and finish in 42 minutes.

After the Capitol 10K in the spring, I was expecting the horde of runners at the start and was prepared for the temptation to join others in walking the hills early in the race. I wasn’t expecting the challenge of running on wet pavement, but I managed to avoid slipping. Alas, after topping the hills at mile 2, I then walked for a tenth of a mile to get my breathing and heart rate back under control.

Despite the shorter distance, my heart rate was considerably higher than during the ten miler earlier in the month:

I ended with a time of 44:06, an 8:49 pace just a hair faster than the ten miler, burning 673 calories in the effort:

So, a set of mixed results: I’m now considering whether I should try running a slow half marathon in December just to prove to myself that I can cover the distance without stressing about my time.

Driving Electric

While my 2013 CX-5 is reasonably fuel-efficient (~28mpg in real world driving), this summer I watched in dismay as gas prices spiked. Even when my tank was almost full, watching prices tick up every time I drove past a gas station left me unsettled. I’d been idly considering getting an electric car for years, but between months of fuel price anxiety and upcoming changes in tax credits (that will leave me ineligible starting in 2023) this fall felt like the right time to finally pull the trigger.

On October 24th, I picked up a new 2023 Nissan Leaf.

I originally shopped for plug-in hybrid SUVs with the intent of replacing my car, but none of the brands seemed to have any available, with waitlists stretching well into next year. So, instead I decided I’d look for a pure-electric to use for daily driving, keeping my CX-5 for family vacations and whenever I need to haul a bigger or messier load. (I worried a bit about the cost to have two cars on my insurance, but the new car added only $30 a month, which feels pretty reasonable.)

I got the shorter-range version of the Leaf (40kwh) which promises around 150 miles per charge. While it’s compact, it makes good use of its interior room, and I have plenty of headroom despite my long torso. The backseat is very tight, but my sons will still fit for a few more years. In the first 25 days, I’ve put about 550 miles on it, and the car has yielded slightly better than the expected 150-mile range. It’s fun to drive. The only significant disappointment is that my Leaf’s low-end “S” trim doesn’t include the smartphone integration to track charging and enable remote start/AC (which would’ve been very useful in Texas summers). Including tax and all of the assorted fees, I paid sticker at $32K (17 down, 15 financed at an absurdly low 2.25%), before discounting the soon-to-expire $7500 federal tax credit.

For the first few weeks, I was trickle-charging the car using a regular 120V (1.4kw) household socket. While 120V takes more than a day to fully charge the Leaf, even slow charging was much more practical for my needs than I had originally expected. Nevertheless, I spent $2550 on a Wallbox Pulsar Plus 40A Level 2 charger ($550 for the charger, $2000 for the new 240V high-amp socket in my garage) to increase the charge speed to the full 6.6kw that the car supports. My current electrical panel only had 30 amps available, which is the max the Leaf will take, but I had the electrician pull a 50 amp wire to simplify things if I ever upgrade to a car with higher capacity. My local electric company will reimburse me $1200 for the charger installation, and there’s also a federal tax credit of 30% capped at $1000. So if everything goes according to plan, L2 charging will only have a net cost of $600.

While I’m enjoying the car, it’s not for everyone– between the small battery and the nearly worthless public fast-charging support, the practical range of the Leaf is low. The Leaf only supports the losing Chademo standard that is likely to go away over the next few years, and the Austin metro area only has two such chargers today. It’s also not clear that the Leaf model line has much of a future; the 2023 edition might be the last, or at least the last before a major redesign.

Nevertheless, for my limited needs, the Leaf is a good fit. In a few years, I expect I’ll replace my CX-5 with a hybrid SUV, but for now, I’m stressing a lot less about gas prices (even as they’ve fallen back to under $3 a gallon in Austin 🤷‍♂️).

-Eric

Thoughts on Twitter

When some of the hipper PMs on the Internet Explorer team started using a new “microblogging” service called Twitter in the spring of 2007, I just didn’t “get it.” Twitter mostly seemed to be a way to broadcast what you’d had for lunch, and with just 140 characters, you couldn’t even fit much more.

As Twitter’s founder noted:

…we came across the word “twitter”, and it was just perfect. The definition was “a short burst of inconsequential information”, and “chirps from birds”. And that’s exactly what the product was.

https://en.wikipedia.org/wiki/Twitter#2006%E2%80%932007:_Creation_and_initial_reaction

When I finally decided to sign up for the service (mostly to ensure ownership of my @ericlaw handle, in case I ever wanted it), most of my tweets were less than a sentence. I hooked up a SlickRun MagicWord so I could spew status updates out without even opening the website, and spew I did:

It looks like it was two years before I interacted with anyone I knew on Twitter, but things picked up quickly from there. I soon was interacting with both people I knew in real life, and many many more that I would come to know from the tech community. Between growing fame as the creator of Fiddler, and attention from improbable new celebrities:

…my follower count grew and grew. Soon, I was tweeting constantly, things both throwaway and thoughtful. While Twitter wasn’t a source of deep connection, it was increasingly a mechanism of broad connection: I “knew” people all over via Twitter.

This expanded reach via Twitter came as my connections in the real-world withered away from 2013 to 2015: I’d moved with my wife to Austin, leaving behind all of my friends, and within a few years, Telerik had fired most of my colleagues in Austin. Around that time, one of my internet-famous friends, Steve Souders confessed that he’d unfollowed me because I’d started tweeting too much and it was taking over his timeline.

My most popular tweet came in 2019, and it crossed over between my role as a dad and as a security professional:

The tweet, composed from the ziplock bag aisle of Target, netted nearly a million views.

I even found a job at Google via tweet. Throughout, I vague-tweeted various life milestones, from job changes, to buying an engagement ring, to signing the divorce papers. Between separating and divorcing, I wrote up a post-mortem of my marriage, and Twitter got two paragraphs:


Twitter. Unquestionably designed to maximize usage, with all of the cognitive tricks some of the most clever scientists have ever engineered. I could write a whole book about Twitter. The tl;dr is that I used Twitter for all of the above (News, Work, Stock) as well as my primary means of interacting with other people/”friends.” I didn’t often consciously think about how much it messed me up to go from interacting with a large number of people every day (working at Microsoft) to engaging with almost no one in person except [my ex] and the kids. Over seven years, there were days at Telerik, Google, and Microsoft where I didn’t utter a word for nine workday hours at a time. That’s plainly not healthy, and Twitter was one crutch I tried to use to mitigate that. 

My Twitter use got worse when it became clear that [my ex] wasn’t especially interested in anything I had to say that wasn’t directly related to either us or the kids, either because our interests didn’t intersect, or because there wasn’t sufficient shared context to share a story in fewer than a few minutes. She’d ask how my day was, and interrupt if my answer was longer than a sentence or two without a big announcement. Eventually, I stopped answering if I couldn’t think of anything I expected she might find interesting. Meanwhile, ten thousand (mostly strangers) on the Internet beckoned with their likes and retweets, questions and kudos.


Now, Twitter wasn’t all just a salve for my crushing loneliness. It was a great and lightweight way to interact with the community, from discovering bugs, to sharing tips-and-tricks, to drawing traffic to blog posts or events. I argued about politics, commiserated with other blue state refugees in Texas, and learned about all sorts of things I likely never would have encountered otherwise.

Alas, Twitter has also given me plenty of opportunities to get in trouble. Over the years, I’ve been pretty open in sharing my opinions about everything, and not everyone I’ve worked for has been comfortable with that, particularly as my follower count crossed into 5 digits. Unfortunately, while the positive outcomes of my tweet community-building are hard to measure, angry PR folks are unambiguous about their negative opinions. Sometimes, it’s probably warranted (I once profanely lamented a feature that I truly believe is bad for safety and civility in the world) while other times it seems to be based on paranoid misunderstandings (e.g. I often tweet about bugs in products, and some folks wish I wouldn’t).

While my bosses have always been very careful not to suggest that I stop tweeting, at some point it becomes an IQ test and they’re surprised to see me failing it.

What’s Next?

While I nagged the Twitter team about annoying bugs that never got fixed over the years, the service was, for the most part, solid. Now, a billionaire has taken over and it’s not clear that Twitter is going to survive in anything approximating its current form. If nothing else, several people who matter a lot to me have left the service in disgust.

You can download an archive of all of your Tweets using the Twitter Settings UI. It takes a day or two to generate the archive, but after you download the huge ZIP file (3gb in my case), it’s pretty cool. There’s a quick view of your stats, and the ability to click into everything you’ve ever tweeted:

If the default features aren’t enough, the community has also built some useful tools that can do interesting things with your Twitter archive.

I’ve created an alternate account over on the Twitter-like federated service called Mastodon, but I’m not doing much with that account just yet.

Strange times.

-Eric

“Not Secure” Warning for IE Mode

A customer recently wrote to ask whether there was any way to suppress the red “/!\ Not Secure” warning shown in the omnibox when IE Mode loads a HTTPS site containing non-secure images:

Notably, this warning isn’t seen when the page is loaded in modern Edge mode or in Chrome, because all non-secure “optionally-blockable” resource requests are upgraded to use HTTPS. If HTTPS upgrade doesn’t work, the image is simply blocked.

The customer observed that when loading this page in the legacy Internet Explorer application, no “Not Secure” notice was shown in IE’s address bar– instead, the lock icon just silently disappeared, as if the page were served over HTTP.

Background: There are two kinds of mixed content, passive (images, css) and active (scripts). Passive mixed content is less dangerous than active: a network attacker can replace the contents of a HTTP-served image, but only impact that image. In contrast, a network attacker can replace the contents of a HTTP-served script and use that script to completely rewrite the whole page. By default, IE silently allows passive mixed content (hiding the lock) while blocking active mixed content (preserving the lock, because the non-secure download was blocked).

The customer wondered whether there was a policy they could set to prevent the red warning for passive mixed content in Edge’s IE Mode. Unfortunately, the answer is “not directly.”

IE Mode is not sensitive to the Edge policies, so only the IE Settings controlling mixed content apply in this scenario.

When the IE Mode object communicates up to the Edge host browser, the security state of the page in IEMode is represented by an enum containing just three values: Unsecure, Mixed, and Secure. Unsecure is used for HTTP, Secure is used for HTTPS, and Mixed is used whenever the page loaded with mixed content, either active or passive. As a consequence, there’s presently no way for the Edge host application to mimic the old IE behavior, because it doesn’t know whether IEMode displayed passive mixed content, or ran active mixed content.

Because both states are munged together, the code that chooses the UI warning state selects the most alarming option:

     content_status |= SSLStatus::RAN_INSECURE_CONTENT;

…and that’s status is treated as a more severe problem:

SecurityLevel kDisplayedInsecureContentWarningLevel = WARNING;
SecurityLevel kRanInsecureContentLevel = DANGEROUS;

Now, even if the Edge UI code assumed the more benign DISPLAYED_INSECURE_CONTENT status, the browser would just show the same “Not secure” text in grey rather than red– the warning text would still be shown.

In terms of what a customer can do about this behavior (and assuming that they don’t want to actually secure their web content): they can change the IE Mode configuration to block the images in one of two ways:

Option #1: Change IE Zone settings to block mixed content. All mixed content is silently blocked and the lock is preserved:

Option #2: Change IE’s Advanced > Security Settings to “Block insecure images with other mixed content”, you see the lock is preserved and the IE-era notification bar is shown at the bottom of the page:

Stay secure out there!

-Eric

Microsoft Employee’s Guide to Maximizing Donations

Perhaps the most impactful perk for employees of Microsoft is that the company will match charitable donations up to a pretty high annual limit ($15K/year), and will also match volunteering time with a donation at a solid hourly rate up to that same cap.

Years ago, I volunteered at a food bank in Seattle, but since having kids I haven’t had time for regular volunteer work (perhaps this will change in the future as they get bigger) so I’ve been focusing my philanthropic efforts on donations.

I donate to a few local charities, but most of my donations are to Doctors Without Borders, an organization that does important, amazing work with frugality and an aim toward maximizing impact.

When I returned to Microsoft, I learned about an interesting method to maximize the amount of money received by the charity without the hassle of trying to send them appreciated stock directly.

It’s simple and convenient, especially if you’re already using Fidelity for your stock portfolio.

  1. Open a “Donor Advised Fund” account at Fidelity Charitable. It’s not free, but at $100 a year, it’s worth it.
  2. Fund that account by moving appreciated shares of stock from your portfolio into the Fidelity Charitable account.
  3. Select how the funds from those shares should be invested (you can pick a low-return bond account, or a higher-return, more volatile index fund)
  4. Whenever you want to donate money to a charitable organization, use a simple form to “recommend a grant” to that organization from your account.
  5. After your grant is sent, visit the Microsoft internal tool to get a match of the amount donated.

Now, if you’re like me, you might wonder why you should bother with this hassle– wouldn’t it be easier to just sell shares and donate the money? Yes, that’s easier, but there are important tax considerations.

First, if you sell appreciated stock, you’re responsible for paying taxes (hopefully at a long-term capital gains rate with the Medicare surtax, so ~18.6% for most of us) on that sale. Then you give all of the proceeds to the charity — you’ll be able to write off what the charity gets as a donation, but that doesn’t include what you’d already paid in taxes.

Second, with the Trump-era tax changes, the Standard Deduction for most of us is now quite high, and the Sales-and-Local-Tax-Deduction cap of $10K means that many of us will barely exceed the Standard Deduction if we donate the MS-Matching-Max of $15000/year. However, here’s where the cool trick comes into play:

  • The IRS grants you the tax deduction of the full value of your appreciated stock when you move that stock to the charitable account.
  • Microsoft matches the value of your donation when you direct a grant to a charity.

What this means is that you can be strategic in the timing of your actions. Move, say, $30000 of appreciated stock into your charitable account, avoiding taxes on your gains because you didn’t “sell” the stock. Write that full amount off on your taxes this year. Then, later in the year, direct $15000 worth of donations out of your charitable account, getting Microsoft to match your donations up to the limit. Wait until next year and grant the other $15000. (You’ll hopefully have some left over for year three due to gains on your charitable account’s investments).

In this way, you can maximize the size of your donations to charity while minimizing the overhead paid in taxes. [1]

-Eric

[1]: I am, generally, an advocate for higher taxes, and certainly for paying what you owe. However, I am fully willing to follow these steps to maximize the chances that my charitable money goes to paying to save lives in the world’s poorest countries and not to padding the pockets of yet another defense contractor.

Q: Why do tabs sometimes show an orange dot?

Sometimes, you’ll notice that a background tab has an orange dot on it in Edge (or a blue dot in Chrome). If you click on the tab, the dot disappears.

The center tab has an orange dot which is not a part of the site’s FavIcon

Why?

The dot indicates that the tab wants “attention” — more specifically, that there’s a dialog in the tab asking for your attention. This might be a JavaScript alert() or confirm() dialog, or a prompt requesting permission to launch an Application Protocol:

Years ago, the dot also used to appear any time the title of a pinned tab changed (because pinned tabs don’t show their titles) but that code was removed in 2018.

Nowadays, web content cannot directly trigger the dot icon (short of showing an alert()) but some sites will draw their own indicator by updating their favicon using JavaScript:

Capturing Logs for Debugging SmartScreen

The Microsoft Edge browser makes use of a service called Microsoft Defender SmartScreen to help protect users from phishing websites and malicious downloads. The SmartScreen service integrates with a Microsoft threat intelligence service running in the cloud to quickly block discovered threats. As I explained last year, the SmartScreen service also helps reduce spurious security warnings for known-safe downloads — for example, if a setup.exe file is known safe, the browser will not warn the user that it is potentially dangerous.

Sometimes, users find that SmartScreen is behaving unexpectedly; for example, today an Edge user reported that they’re seeing the “potentially dangerous” warning for a popular installer, but no one else has been able to reproduce the warning:

Download warning should not show if SmartScreen reports the file is known-safe

After quickly validating that SmartScreen is enabled in the system’s App & Browser Control > Reputation based protection settings panel:

…we asked the user to confirm that SmartScreen was generally working as expected using the SmartScreen demo page. We found that SmartScreen was generally performing as expected (by blocking the demo phishing pages), so the problem is narrower than a general failure to reach the SmartScreen service, for example.

SmartScreen Logging

At this point, we can’t make much progress without logs from the impacted client. While Telerik Fiddler is a good way to observe traffic between the Edge client and the web service, it’s not always the most convenient tool to use. Historically, SmartScreen used a platform networking stack to talk to the web service, but the team is in the process of migrating to use Edge’s own network stack for this communication. After that refactoring is completed, Edge’s Net Export feature will capture the responses from the SmartScreen service (but due to limitations in the NetLog format, the request data sent to SmartScreen won’t be in those logs).

Fortunately, there’s another logging service in Edge that we can take advantage of– the edge://tracing feature. This incredibly powerful feature allows tracing of the browser’s behavior across most of its subsystems, and it is often used for diagnosing performance problems in web content. But relevant to us here, it also allows capturing data flowing to the SmartScreen web service.

Capture a SmartScreen Trace

To capture a trace of SmartScreen, follow these steps:

  1. Start Microsoft Edge and navigate to edge://tracing
  2. Click the Record button:

3. In the popup that appears, choose the Manually select settings radio button, then click the None button under Record categories to clear all of the checkboxes below it:

4. Scroll down the list of categories and place a checkmark next to SmartScreen

5. At the bottom of the popup, push the Record button:


6. A new popup will appear indicating that recording has started.

7. Open a new tab and perform your repro (e.g. visit the download page and start the download. Allow the download to complete).

8. In the original tab, click the Stop button on the popup. The trace will complete and a trace viewer will appear.

9. Click the Save button at the top-left of the tab:

10. In the popup that appears, give the trace a meaningful name:

11. Click OK and the new trace file will be saved in your Downloads folder with the specified name, e.g. SmartScreenDownloadRep.json.gz
12. Using email or another file transfer mechanism, send this file to your debugging partner.

Thanks for your help in improving our service!

-Eric

PS: Your debugging partner will be able to view the SmartScreen traffic by examining the raw JSON content in the log. If you’d like to poke at it yourself, you can look at the data by double-clicking on one of the SendRequestProxy bars in the trace viewer that opened in Step #8:

Cruising Alaska (Alaskan Brews Cruise)

I lived in the Seattle area for nearly 12 years, and one of my regrets is that I never took advantage of any of the Alaskan cruises that conveniently leave from Pier 91 a few miles out of downtown. Getting to Alaska from Austin is more of a hassle, but I figured I’d pair it with a visit to work and friends, so I booked Royal Caribbean’s “Endicott Arm & Dawes Glacier Cruise”, departing Seattle on September 16th. While there were a lot of moving parts (two rental cars, two hotel stays, a workday, friend visits, mandatory COVID testing, Canadian entry paperwork), nearly everything went according to plan… and yet almost nothing was as I’d expected. My expedition mate for this voyage was Clint, one of my two oldest friends– we’ve been going on adventures together since high school.

We started with the flight to Seattle, an early morning departure on Alaska Airlines, paid for entirely with points I’ve accumulated over twenty years (thank goodness their mileage plan’s points never expire– I accumulated almost all of these points over a decade ago). I drove to the office and visited with folks on my new team and we headed out to lunch at Matador, an old favorite in downtown Redmond. After work, Clint and I met up with Chris, one of my good friends from way back in Office days (circa 2002-2004)– we sampled some of the beers at Black Raven in Redmond. The following morning, I walked over to the Peet’s Coffee in Redmond, another old favorite where I had started writing the Fiddler book.

After coffee and free breakfast at the hotel, and a mandatory COVID test supervised online, we headed over to Seattle, dropped off our rental car at the Space Needle, and took a quick Lyft out to Pier 91 and our boat, the Ovation of the Seas. It was big. Too big, arguably– it doesn’t look like a boat so much as an apartment building afloat. (I really liked the Adventure of the Seas, my vessel for my first two Royal cruises) I was excited to see the ship, but first we had to get through an annoyingly long queue. I’d read some posts about the boarding process in Seattle, so I thought I was prepared, but what I wasn’t prepared for was the paper handed out at the front of the line… it turned out that our glacier cruise wasn’t going to be a glacier cruise after all. Boo!

Since I didn’t have any particular expectations for the glacier viewing, I was mostly just annoyed– the daylight hours of any spot on earth have been calculable for hundreds of years, so none of this should have been surprising to the planners. (A few days later, the Captain did a little presentation and mentioned that on the prior cruise, fog meant that their approach to the glacier was aborted three miles out, so no one really got to see much. Fog, at least, seems a less predictable phenomenon than daylight.)

No matter, we were here, COVID free, and going to board the boat. We’d packed wisely and headed to the Windjammer buffet dining room for lunch and snacks while our luggage was loaded onto the ship. At 2PM, we got access to our room. It was nice, although they hadn’t yet split the twin beds and it was tight compared to the junior suite I’d shared with the kids on the Adventure of the Seas in March.

The balcony was a good size, although given the weather forecast (rainy and low 50s) I wasn’t sure how much I’d be using it, even with the cozy blanket I’d packed, and apple cider and hot chocolate packets I’d brought to use in the room’s kettle.

Ultimately, our balcony was mostly home to my sweaty workout clothes after my one run in the ship’s gym. Unlike on the Caribbean cruise, they didn’t dry out. :)

As we waited for our 4PM departure, we were treated to some beautiful views of Seattle and Puget Sound:

The ship was in great shape and nicely decorated, although we were quickly reminded about how inadequate the elevators are (slow, crowded) and this was even more of an issue on the enormous Ovation. We ended up climbing a lot of stairs over the week between our home (cabin 8690) on Deck 8, the shows and main dining room on 3, and topside at 14. Fortunately, the stairwells were decorated with some fun art to break up the monotony:

One of the most visible features on the Ovation of the Seas is its “North Star” observation pod which extends on an arm up to 300 feet above sea level.

I didn’t want to miss it, so we ended up booking one of the first slots, going up before we’d even undocked.

Ultimately, it mostly ended up being a good way to see the whole ship– 300 feet sounds like a lot, but when you’re miles away from any points of interest, it doesn’t make much of a difference. (It probably would’ve been great late in the trip if I’d been excited about whale watching)

After unpacking, dinner, and the “Welcome aboard” Comedy show, we watched a movie (The 335) in the open air on the top deck (chilly!) and went to bed.

Our first full day was a Day at Sea, where I explored the ship, read a book, enjoyed the food, and generally relaxed. The ship was well-designed for this itinerary– while the kids water features were limited (the kids would’ve been very disappointed in the tiny water slides), there was an arena where you could ride bumper cars, roller skate, or play dodge ball, a small climbing wall, a small iFly indoor skydiving tube, and a ping-pong and an XBOX gaming lounge (although more than half of the consoles were broken. Sad).

For the grownups, there was an amazing solarium with hot tubs, lounge chairs, and little snuggle pod couches:

Two of my favorite spots were the two “bridge extension rooftops” that extended across the bow:

These allowed a look back at the rest of the ship; our cabin was somewhere around the orange arrow:

Throughout the cruise, I spent quite a bit of time walking laps on the top deck, passing by some really impressive decorations:

Dinner in the dining room was “Formal Night” so we dressed up in our best. Unlike the dining room in the Adventure of the Seas (a wide-open three-story beauty), our main dining room on the Ovation felt dark and claustrophobic, despite (or perhaps partly because of) mirrors mounted in the ceiling. (The Ovation splits its “main dining room” into four single-story areas). Our waiter seemed extremely stressed for the entire cruise, and all of our interactions felt extremely awkward.

After dinner, we saw the first big song-and-dance show, the Vegas-style “Live, Love, Legs.” The ability to see a great live show is one of my favorite things in the world and I ended up watching it twice, first from the balcony at 8pm and then from the front row at 10pm. The performers were super-talented, and it was awesome to get to see the show from good seats.

When I woke up early the next morning, I was excited to grab breakfast and get my first-ever glimpse of Alaska. I grabbed breakfast at the buffet and walked out the doors to the patio bracing for the cold… but it was only chilly at worst. While undeniably beautiful, everything looked a bit like, well, everywhere else in the Pacific Northwest.

Ah well. After breakfast, I was excited to get out and explore Ketchikan, Alaska’s “First City”:

Now, it’s worth explaining here that I didn’t really have a plan, per-se, for any port on this cruise. While the idea of buying the ship’s expensive “unlimited drinks” package (making this a “booze cruise”) sounded depressing and risky, the notion of doing a “brews cruise”, hitting the breweries in each port-of-call, sounded like a lot more fun.

Besides, by the time I had started looking into booking excursions for this trip, most were sold out, all were obscenely expensive (hundreds of dollars per person for most of them) and the weather was supposed to be awful anyway. So, I was excited to get out to discover whatever there was to see.

As we got off the ship, we were handed the little “Here are some shops you should check out” brochure that had a tiny map. On the map was a mention of hiking trails, so we set out in that direction. We walked a few miles on the road along the water until we reached the Ferry Terminal (oops, too far) and turned around to head back to the trailhead at the University of Alaska Southeast.

After a pretty but short hike, with some lovely overlooks:

….we were unceremoniously dumped back out on a (admittedly beautiful) back road and we walked back to the city, past the beautiful Ketchikan Public library and the less-beautiful Ketchikan jail.

Back in town, we grabbed coffees and pondered our next move. Lunch? We headed to a local fisherman’s bar, where we didn’t find anything interesting to eat or on tap, but I got to enjoy an old favorite in its home port:

Nothing in town seemed like a “Can’t miss” for lunch, so we decided to pop back onto the boat to try the Halibut and Chips at the “Fish and Ships” restaurant atop the boat. Frustratingly, they didn’t have the Halibut (and wouldn’t for the entire trip, despite remaining on their digital menu screen, grrr) so we settled for plain cod.

“50s and raining? Naw 70s and sunny!”

We then got back off the boat to find more beer. We ended up at a fantastic bar (Asylum) which had a huge selection on tap, including “Island Ale“, an instant favorite that I subsequently failed to find again for the rest of the trip :( .

We enjoyed our drinks with some pickle popcorn on nice sunny patio with a view out over the water. Alas, our ship’s 4pm departure drew near and we stumbled happily back to the boat. I chilled with my book on the top deck and didn’t even notice as we started pulling away.

After dinner, I spent some time reading alone on deck.

The next morning, I woke up early and headed down to breakfast. The fog over the water gave everything an otherworldly quality and I enjoyed a second cup of coffee walking the deck as we pulled into Juneau.

After disembarking, we immediately booked sets on a bus out to the Mendenhall Glacier, a short trip away. We spotted a half-dozen bald eagles (“Golf ball heads”) along the road, mostly watching us from the top of lampposts. The tour guide pointed out the local McDonald’s, noting that it was the only one that some local rural folks would see on rare trips to “the big city”.

Now, I’ll confess here that I had made it 43 years on this rock called Earth under the misimpression that a glacier is just an especially big iceberg, which turns out not to be the case at all. So, I was a bit surprised and disappointed, but nevertheless agreed that it was a beautiful sight. We hiked out to the base of the 377-foot Nugget Waterfalls at the right of this picture:

…and posed along the way with some ice that had taken hundreds of years to reach this shore:

I even carefully selected an icecube to bring home to the kids as a souvenir:

After a few hours, we’d walked all of the shorter trails and rain threatened, so we boarded the bus back to town.

In the city, we took our bus driver’s advice for a good spot for Halibut and Chips (crazy expensive at 30$ a plate: not bad, but not worth it either), bought some postcards to send home, and went in search of a brewery. We started at Devil’s Club Brewing, a nice-looking spot with some interesting (somewhat exotic) beers.

After a flight and another pint of our favorites, we mailed my postcards and found a more traditional bar where I had a hazy IPA and Clint paired a Guinness with an Alaskan Duck Fart.

We then headed back to the ship for dinner, deciding at the last minute to walk a half mile up the coastline to where a famous whale fountain had been installed in a park a few years ago. It was worth the walk, although it looked considerably less lifelike in person. :)

The Fountain
View from the park

After dinner and with hours to kill before Ovation’s 10PM departure, the neon “Alaskan Brewing” sign at the taproom next to the boat beckoned and we decided to head off for another drink.

View from the taproom

After sitting for almost ten minutes without a waitress in sight, we left to find a more fruitful taproom. (As we walked out to the street, we realized that we’d entered the back of the place and that’s probably why there was no service). We ended up at the cozy taproom (they had a cat!) for Barnaby Brewing, one of my favorites of the entire trip, and I enjoyed several delicious selections.

We closed the place down (admittedly, at 8pm) and headed back to the ship.

We had an early 7am arrival at our final Alaskan destination, Skagway, but because of some damage to the dock we had to use tenders (small boats) to reach the shore. On past cruises, this has been very cumbersome, but given the short distance, enormous tenders, and lack of competition for slots, it turned out to be trivial.

Again, I had no plan for what we might do in Skagway. It seemed like the most popular excursions involved getting on a train and riding it around, a prospect I found less than exciting. Fortunately, Google Maps reconnaissance indicated not one but two breweries in this tiny town.

We started by walking from one end of the city to the other, and grabbing a “Honey Bear Latte” at a cute little coffee shop (which was, unsurprisingly, flooded with tourists).

We bought a few souvenirs (shirts and a hat) then found our way to the Skagway Brewing Company, where we had a pint before heading upstairs for another lunch of Halibut and Chips (again, crazy expensive, and again, not really worth the price).

We then headed over to Klondike Brewing Company for a few tasty drinks:

… and then shuttled back to the boat before Ovation’s 6pm departure. The rain held off, and I ended up lounging on deck as we shoved off.

That night, the show was “Pixels”, a singing/dancing/multimedia spectacle in the “270 Lounge” at the back of the ship. It was a short show, and while entertaining, I didn’t enjoy it nearly as much as the other shows.

The next day was the second “Sea Day” with no ports-of-call, so I headed to the gym in the morning to run up an appetite– we were slated to have lunch at the steakhouse. Running was hard– I ended up splitting my 10K into two 5Ks with a few laps on the deck in the middle. My knees have been threatening me for the last few weeks, and the treadmills in the gym weren’t in great shape. I’ve also grown accustomed to running with multiple big fans pointed directly at me, and the ship felt hot and claustrophobic by comparison.

Lunch was, alas, a miss. Through some sort of scheduling mixup, our lunch was actually a “Taste of Royal” tasting tour, where we sat in the fancy “Wonderland” restaurant and had one plate from each of the “premium” eateries on ship. So, rather than a giant steak, we had a fancy spritzer drink, a tiny fish course, a tiny risotto dish, a tiny steak, and a small piece of fried cheesecake. It was tasty, but not what I’d run six miles for.

We putzed around all afternoon, had dinner, and watched a talented singer (Ana Alvaredo) covering popular songs at the onboard pub, Amber and Oak. But the big event of the day was the night’s show in the main theater, The Beautiful Dream. It was, in a word, spectacular. The costumes were amazing. The song choices (a mix of 80s/90s) were perfect. The singing and dancing were powerful. The plot (A father of two loses his wife and must find a way to carry on, was perhaps a bit too on the nose).

I was blown away and resolved that I must make more of an effort to see live theater. After seeing it close up at the 8pm showing, I went back to sit in the balcony at the 10pm showing to take it all in.

I went to bed glowing… this show alone was worth the trip.

Our final full day featured Victoria, but with a slated arrival time of 5pm, we had a day to fill on the boat first. I spent a few hours in hot tubs while most of the passengers were below decks.

Given our evening arrival (and sundown a scant 135 minutes later) I worried that it might not be worth even getting off the boat. In particular, I assumed that getting cleared off the ship and out of the port would be a hassle based on a blog from June, but it was the opposite– nobody checked our passports, vaccination status, arrival forms, or anything else. We all just walked off the boat and through the “Welcome to Victoria” building.

After a short walk along the coastline, we found ourselves in the middle of plenty to do. We quickly found the amazing Refuge Tap Room, where I got two beers and a flight, including a delicious apricot wheat. After drinks, we stopped for a quick, tasty, and calorie-laden poutine and then headed back to the ship.

I had a lot more fun in Victoria than I expected.

We arrived and disembarked in Seattle the following morning. We grabbed fancy Eggnog Lattes at Victor’s Coffee Company, took a long walk in one of my favorite parks (Marymoor), before lunch at one of my favorite spots (Ooba Tooba) and then went out for drinks at Black Raven with Nick. We finished the day with Thai Ginger for dinner.

On Saturday, we went to visit our friends Anson and Rachel in Bothell, then checked out the taproom for Mac & Jacks (my favorite beer). After a few drinks there, Zouhir introduced us to Chicago Pastrami in Issaquah, where I had an amazing Reuben and delicious pistachio ice cream.

After I posted the M&J pictures online, everyone said we had to try out Postdoc Brewing just down the street. So, we did the following day before we headed to the airport.

Our trip back was uneventful; we got to the airport super-early after reading horror stories of three-hour security lines at SeaTac, but I breezed through the TSA Pre line in less than fifteen minutes. We had plenty of time to get one last Mac & Jacks at the Africa Lounge, my favorite way to depart Seattle.

Our flight landed around midnight Austin time, and I eagerly tumbled into bed around 1:30 on Monday morning.

All in all, it was an amazing trip that I largely did not appreciate fully. I’ve got a lot on my mind.

Miscellaneous notes:

  • Seven days is too long for me to cruise without kids or a significant other to get me out of my head.
  • Having cell service on the trip made cruising feel very different. While it was convenient to post photos and hunt breweries ahead of time, it really changed the vibe for the worse.
  • Ships can be too big.
  • Cruise-ship comedians aren’t very funny unless you’re drinking.
  • Back home, I miss the fancy desserts.

-Eric

HTTPS Goofs: Forgetting the Bare Domain

As I mentioned, the top failure of HTTPS is failing to use it, and that’s particularly common in in-bound links sent via email, in newsletters, and the like.

Unfortunately, there’s another common case, whereby the user simply types your bare domain name (example.com) in the browser’s address bar without specifying https:// first.

For decades, many server operators simply had a HTTP listener sitting at that bare domain, redirecting http://example.com to https://www.example.com, changing from insecure HTTP to secure HTTPS and redirecting from the apex (base) domain to the www subdomain.

However, providing HTTPS support on your www subdomain isn’t really enough, you must also support HTTPS on your apex domain. Unfortunately, several major domains, including delta.com and royalcaribbean.com do not have HTTPS support for the apex domain, only the www subdomain. This shortcoming causes two problems:

  1. It means you cannot meet the submission requirements to HSTS-Preload your domain. HSTS preloading ensures that non-secure requests are never sent, protecting your site from a variety of attacks.
  2. Users who try to visit your bare domain over HTTPS will have a poor experience.

This second problem is only getting more common.

Browsers are working hard to shift all traffic over to HTTPS, adding new features to default to HTTPS for user-typed URLs (or optionally even all URLs). For some sites, like https://delta.com, the attempt to navigate to HTTPS on the apex domain will very slowly time out:

…while for other sites on CDNs like Akamai (who do not seem to support HTTPS for free), the user gets a baffling and scary error message because the CDN returns a generic certificate that does not match the target site:

It’s frustrating to me that Akamai even offers a “shoot self in foot” option for their customers when their competitors like Cloudflare give HTTPS away, even to sites on their free tier who don’t pay them anything.

Ideally, sites and CDNs will correct their misconfigurations, helping keep users secure and avoiding confusing errors.

On the browser developer side, it’s kinda fun to brainstorm what the browser might do here, although I haven’t seen any great ideas yet. For example, as I noted back in 2017, the browser used to include a “magic” feature whereby if user went to https://www.example.com but the cert only contained example.com, the user would be silently redirected to https://example.com to avoid a certificate error. You could imagine that the browser could introduce a similar feature here, or we could ship with a list of broken sites like Delta and Royal Caribbean and help the user recover from the site’s configuration error. Unfortunately, most of these approaches don’t meet a cost/benefit bar, so they remain unimplemented.

Please ensure that your apex domain loads properly over HTTPS!

-Eric