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; preloadmax-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-requestsFor 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 | |
|---|---|---|
| Layer | Transport, how the browser reaches your host | Content, URLs loaded inside your page |
| Scope | Your host and subdomains you send it for | Every subresource, including third-party hosts |
| Top-level navigation from an external link | Upgraded, once known or preloaded | Not covered |
| Third-party mixed content | Not covered | Upgraded |
| Threat | SSL stripping, protocol downgrade | Mixed content on the page |
| On failure | Certificate errors are fatal, no click-through | Upgraded request fails, no HTTP fallback |
| Where it is set | Strict-Transport-Security response header | CSP directive |
| Support | All 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
- RFC 6797, HTTP Strict Transport Security
- W3C, Upgrade Insecure Requests
- MDN, Strict-Transport-Security
- MDN, upgrade-insecure-requests
- MDN, Mixed content
- hstspreload.org, submission requirements and removal
- OWASP, HTTP Strict Transport Security cheat sheet