Why Did Chrome Block My Download When Everything Should Be Secure?
I was testing my résumé shortlink, rakha.dev/l/resume, by typing it directly into Chrome's address bar. The download was blocked. The message said the site wasn't using a secure connection. Everything in that path should have been secure: the site is HTTPS only, and the file lives on Google Drive.
The link redirected with a 301 to a Google Drive download link.
Everything looked fine
Did I misconfigure the redirection or the HTTPS config? I checked the certificate, then checked it again. curl -I on the HTTP address returned a redirect to HTTPS, and the HTTPS response carried a strict-transport-security header. .dev is on the HSTS preload list, so Chrome upgrades the initial HTTP address before sending a request. The redirects after that stayed on HTTPS.
Every check passed, and the warning kept appearing.
Then I found a URL that had nothing to do with my site. In a fresh Chrome profile, http://go.dev/dl/go1.22.0.src.tar.gz is blocked with the same message. Go ahead, click it in Chrome to see the block yourself. The https:// form of the same URL downloads normally. That gave me a reproduction outside my setup; whatever was happening was not unique to my server or certificate.
Typing https:// made it work
The difference was what I typed. rakha.dev/l/resume failed. https://rakha.dev/l/resume worked.
I started debugging it with an agent: reproduce the block, then trace the root cause. Was it my redirect configuration, or, as I suspected, bad defaults in the browser? We ran the failing and working URLs side by side in fresh profiles, and the agent traced the decision to Chromium's download classifier, which builds an initiator from the download item or the tab and checks the URL chain with a "potentially trustworthy" predicate. I did not read the whole file myself; I used that to form a hypothesis and tested it against the cases.
On desktop, Chrome resolves an address without a scheme to http://. Because of HSTS, it upgrades that address before any request leaves the browser, so the request is HTTPS. The original address stays in the record the browser keeps for the navigation, though, and the download check looks at that record.
The classifier does not consult whether HSTS upgraded the connection.
Why the check exists
Downloads persist, and someone may run the file later. If an attacker can change the page that starts a download, they can change where its link points, even when the eventual download uses HTTPS. Google's post announcing the policy names the risk directly: an insecure download can be swapped for malware, or read by an eavesdropper.
Ignoring an HTTP starting address just because the file eventually arrives over HTTPS would be too broad. A server-side HTTP-to-HTTPS redirect still involves a plaintext request. In my reproduction, the upgrade happened inside the browser before any request.
What I changed
I could have left it there and told people to type https://. I wanted the shortlink to work when typed as it is, without requiring the scheme.
The short links on my site now return two different responses. Ordinary requests get a redirect. Navigations without a website initiator, like typing an address or opening a bookmark, get a small HTML page instead.
It comes down to one header, Sec-Fetch-Site: none, which browsers set when a page didn't start the navigation. Everything else gets the redirect. If you type the URL directly, you get a small page that redirects to the destination with JavaScript, so the record no longer begins with http://.
I wrote the function with the same agent and checked both cases in a fresh profile and against the deployed site. Typing the URL now downloads the file. The cost is one extra hop for typed and bookmarked links.
I had been checking the certificate and HTTPS redirects. What I had missed was the difference between the address I typed and the request Chrome actually sent.
I filed it on the Chromium issue tracker, with the go.dev reproduction. At the time of writing, it's still fresh and unconfirmed.
I don't know whether Chromium will change the check. Until then, the link works with https://, and it works without the scheme because of the new handoff. That is more effort than a short link should need, but it is the part I could fix.