All posts

HSTS vs upgrade-insecure-requests, do you need both

CentralCSP Team ·

HSTS forces every navigation to your host over HTTPS. The CSP upgrade-insecure-requests directive rewrites insecure subresource URLs inside the page, including third parties. They are complementary, not alternatives, and a site that wants no insecure requests at all sends both.

People often treat them as two ways to do the same thing, force HTTPS. They are not. HTTP Strict Transport Security (HSTS) controls how the browser reaches your host. It makes every navigation to your domain go over HTTPS, including a click from an external link, once the browser knows the host. The CSP upgrade-insecure-requests directive controls the URLs your pages load once they are open. It rewrites insecure subresource requests inside your document, including to third-party hosts that HSTS cannot touch. They overlap almost nowhere, which is why you send both.

HSTS vs upgrade-insecure-requests, two halves of forcing HTTPS

Think of two separate questions. First, when a browser goes to your site, does it connect over HTTPS or does it start with a plaintext hop an attacker can strip? That is a transport question about reaching your host. Second, once your page is loaded, do the resources it pulls in, your own and third parties', all load over HTTPS, or does a stray http:// image trigger mixed content? That is a content question about what runs inside the page.

HSTS owns the first question. upgrade-insecure-requests owns the second. Neither answers the other.

HSTS secures how the browser reaches your host

HSTS is a response header, defined in RFC 6797, that a site sends over HTTPS to tell the browser "for the next N seconds, only ever talk to this host over HTTPS." The browser caches your domain as a Known HSTS Host, rewrites any http:// URL for it to https:// before the request leaves the machine, and hard-fails on certificate errors with no click-through.

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

max-age is the required lifetime in seconds, and every HTTPS response that carries the header resets the clock, so regular visitors never fall out of the policy. Sending max-age=0 over HTTPS deletes the cached entry, which is the escape hatch if you set the header by mistake. The browser ignores the header entirely when it arrives over plain HTTP; if it did not, an attacker sitting on the insecure hop could set or clear your policy for you. HSTS also only works for domain names, an IP address cannot be a Known HSTS Host.

includeSubDomains extends the policy to every subdomain, downward from the host that sent it. OWASP's rationale for it is worth keeping in mind: a session cookie scoped to the parent domain can be manipulated from an insecure subdomain, so covering the whole tree is what actually protects the session.

What this stops is protocol downgrade, also called SSL stripping: an attacker on the network turning your HTTPS connection into a plaintext one, or spoofing a certificate. Because the browser upgrades before the request goes out and refuses to let the user click past a TLS error, there is no insecure hop to strip. The one gap is the very first request to a host the browser has never seen, RFC 6797 calls it the bootstrap MITM vulnerability, and preloading closes it.

The key scope limit: HSTS only covers hosts you control and that send the header. It has nothing to say about a third-party domain your page happens to load a script from.

Preload, and why you ramp into it

preload is the opt-in signal for the browser preload list at hstspreload.org, which ships the policy inside the browser itself so even a first-ever visit is protected. It is not part of RFC 6797; it is a de facto mechanism run through Chrome's list that other browsers consume. Submission requires a valid certificate, an HTTP-to-HTTPS redirect on the same host, every subdomain served over HTTPS, a max-age of at least one year, both includeSubDomains and preload in the header, and the header present on the HTTPS redirect itself.

Those requirements are also the trap. Preload plus includeSubDomains forces HTTPS on every subdomain, including the internal or forgotten ones, for every user of every browser that ships the list. Undoing it is slow: you drop the preload directive, request removal, and then wait, since removal takes 6 to 12 weeks to reach most Chrome users and longer for other browsers, because the list is hardcoded into browser releases. So ramp in stages, as hstspreload.org itself prescribes: max-age=300, then 604800 (a week), then 2592000 (a month), waiting out each max-age before raising it, and only then commit to the year-plus value with preload.

upgrade-insecure-requests fixes mixed content inside your pages

upgrade-insecure-requests is a Content Security Policy (CSP) directive. When a page carries it, the browser rewrites every insecure http:// subresource URL in that document to https:// before the request is made, first-party and third-party alike: images, scripts, styles, frames, fetches. Navigations are handled more narrowly. The browser upgrades navigations to your own host, form submissions, and nested frames, but leaves links to third-party sites alone; the spec's authors judged that rewriting someone else's navigation URL carries too much breakage risk, so a plain link to http://other.example/ is requested as written.

Content-Security-Policy: upgrade-insecure-requests

For subresources, the reach is the opposite of HSTS. It applies to every URL your page loads, including third-party hosts you do not own and could never send HSTS for. If your page references http://widgets.example.net/w.js, HSTS on your own domain does nothing for it, but upgrade-insecure-requests rewrites it to https:// before the request is made. That is how you clear mixed-content warnings across a page that pulls from many origins without editing every URL by hand.

The rewrite is blind, and there is no fallback. If a resource is not actually available over HTTPS, the upgraded request fails and the browser never retries over HTTP. The deployment prerequisite is that everything your pages load is served at the same URL on both schemes.

It also helps to know the baseline you are improving on. Modern browsers already auto-upgrade what the Mixed Content spec calls upgradable content: images, audio, and video loaded through src, plus CSS images. Everything else insecure, scripts, stylesheets, iframes, fetch and XHR calls, fonts, is blocked outright rather than loaded. So without the directive, an http:// script is not a silent hole, it is a broken page. What upgrade-insecure-requests adds is that this blockable content gets upgraded instead of blocked, and the console stops filling with mixed-content warnings. One edge case survives either way: mixed content addressed by an IP address is blocked, not upgraded. The deprecated sibling directive, block-all-mixed-content, took the opposite approach and blocked this content instead of upgrading it.

Why neither replaces the other

They solve different problems, so one cannot stand in for the other, and the specs say so themselves. MDN states that upgrade-insecure-requests "does not replace the Strict-Transport-Security (HSTS) header," and the W3C spec is even more direct: the directive "does not deprecate, replace, or in any way reduce the value of" HSTS. Upgrading the URLs inside a page does nothing about the top-level navigation that loaded the page in the first place. A user clicking an http:// link to your site from somewhere else needs HSTS to be upgraded before the connection is made; the CSP directive never runs, because the page has not loaded yet.

Run it the other way and the gap is just as clear. HSTS upgrades requests to your own host, so a stray http:// reference to your own domain gets fixed, but a third-party http:// subresource is outside its scope entirely. Only upgrade-insecure-requests reaches that.

What each covers

Strict-Transport-Security (HSTS)upgrade-insecure-requests
LayerTransport, how the browser reaches your hostContent, URLs loaded inside your page
ScopeYour host and subdomains you send it forEvery subresource, including third-party hosts
Top-level navigation from an external linkUpgraded, once known or preloadedNot covered
Third-party mixed contentNot coveredUpgraded
ThreatSSL stripping, protocol downgradeMixed content on the page
On failureCertificate errors are fatal, no click-throughUpgraded request fails, no HTTP fallback
Where it is setStrict-Transport-Security response headerCSP directive
SupportAll modern engines (Blink, Gecko, WebKit)All modern engines (Blink, Gecko, WebKit)

Recommendation

Send both. Set HSTS with a long max-age and includeSubDomains, ramp the max-age in stages, and add preload only once every subdomain is confirmed on HTTPS, so navigations to your host are locked down. Add upgrade-insecure-requests to your Content Security Policy so every subresource, including the third parties HSTS cannot reach, loads over HTTPS, after checking that those resources actually exist on HTTPS, since a failed upgrade does not fall back. Together they cover the full path: HSTS gets the browser to your host securely, and the CSP directive keeps everything the page then loads on HTTPS.

To confirm both are present and correctly formed across your site, run CentralCSP's free security headers scanner. It flags a missing or weak HSTS header and shows whether your policy carries upgrade-insecure-requests.

The upgrade half is worth watching after the scan too. An upgraded request that has no HTTPS version fails outright, and nothing in the page tells you which third party broke. Because the failure is a network error, a CentralCSP endpoint receiving NEL reports surfaces those hosts from real traffic, which is how you find the resource that only exists on HTTP before your visitors do.

FAQ

Does upgrade-insecure-requests replace HSTS?

No. The directive only rewrites URLs inside a page that has already loaded, so it cannot upgrade the navigation that brings a user to your site from an http:// link elsewhere. Both MDN and the W3C spec say to keep HSTS set alongside it; the spec's wording is that the directive does not "deprecate, replace, or in any way reduce the value of" HSTS.

Do I need both HSTS and upgrade-insecure-requests?

Yes, they cover different halves. HSTS pins how browsers reach your host across visits, and on the first visit too once preloaded, which the CSP directive can never do. upgrade-insecure-requests upgrades the in-page subresources, including the third-party hosts your HSTS header cannot cover.

How does upgrade-insecure-requests fix mixed content?

It tells the browser to rewrite every insecure http:// subresource URL in the page, first-party and third-party, to https:// before the request is made, so images, scripts, styles, and frames load securely instead of being blocked or flagged as mixed content. There is no fallback: a resource that is not available over HTTPS simply fails.

Is HSTS preload safe?

It is safe once every subdomain is confirmed on HTTPS, but it is close to irreversible on a human timescale. Preload requires a one-year minimum max-age with includeSubDomains, so every subdomain, including internal ones you forgot about, is forced to HTTPS for all users. Removal from the list takes 6 to 12 weeks to reach most Chrome users, longer for other browsers. Ramp max-age in stages (300, then 604800, then 2592000) before committing.

Sources