HSTS: The One Header That Would Have Prevented the Attack
HSTS: The One Header That Would Have Prevented the Attack
Your app is served over HTTPS. Your hosting provider handles the certificate. You have a padlock in the address bar. You are secure.
Except for the first request.
The attack
A user opens their laptop at a coffee shop and connects to the WiFi. They type yourapp.com in the address bar — not https://yourapp.com, just the domain. The browser makes an HTTP request to port 80. Your server redirects to HTTPS.
But between the HTTP request and the HTTPS redirect, the attacker on the same WiFi network intercepts the plaintext request. They serve a forged version of your login page over HTTP. The user enters their credentials. The attacker now has them.
This is not theoretical. It is called an SSL stripping attack. Moxie Marlinspike demonstrated it at Black Hat 2009 with a tool called sslstrip. The tool is still effective against any site that does not set one specific header.
That header is Strict-Transport-Security.
What HSTS does
HTTP Strict Transport Security (HSTS) tells the browser: "For the next N seconds, never make an HTTP request to this domain. Always use HTTPS, even if the user types http:// or clicks an http:// link."
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
After the browser sees this header once (over a valid HTTPS connection), it remembers. Every subsequent request to your domain skips HTTP entirely. The browser internally redirects http:// to https:// before any network request is made. There is nothing for an attacker to intercept.
The three directives:
- max-age=31536000 — remember this policy for 1 year (in seconds)
- includeSubDomains — apply to all subdomains too, not just the apex
- preload — eligible for browser preload lists (more on this below)
The benchmark data
We checked security-headers.transport.hsts-enabled across 30 open-source projects. Only 6 properly configure HSTS:
HSTS configured (6):
- Supabase
- Dub
- Cal.com
- Infisical
- Plausible
- Documenso
HSTS missing (24): Everyone else. Including projects scoring above 70 overall. Including projects with otherwise solid security posture. Including projects handling sensitive data.
This is the most commonly missed critical-adjacent security check in our entire benchmark. HTTPS enforcement (the parent check) has a higher pass rate because many hosting providers handle it automatically. But HSTS — the header that makes HTTPS enforcement permanent in the browser — gets skipped.
"But my host already does HTTPS"
Yes. Vercel, Netlify, Cloudflare Pages, and most modern hosting platforms enforce HTTPS and automatically redirect HTTP to HTTPS. That handles the server-side enforcement. But it does not set the HSTS header by default on all platforms, and even when it does, the first visit is still vulnerable.
Here is the timeline without HSTS:
- User types
yourapp.com - Browser sends HTTP request to port 80
- Attacker intercepts (or your server redirects to HTTPS)
- Browser receives HTTPS response
- Repeat from step 1 on next visit
Here is the timeline with HSTS:
- User types
yourapp.com(first visit ever) - Browser sends HTTP request to port 80
- Server redirects to HTTPS and sends HSTS header
- Browser remembers: this domain is HTTPS-only
- User types
yourapp.com(any future visit) - Browser internally upgrades to HTTPS — no HTTP request ever leaves the machine
The first visit is still vulnerable. That is why HSTS preloading exists.
The preload list
Browsers maintain a hardcoded list of domains that should always use HTTPS. If your domain is on this list, even the first visit is protected — the browser knows to use HTTPS before it has ever connected to your server.
To get on the preload list:
- Set the HSTS header with
preloaddirective - Set
max-ageto at least 1 year (31536000) - Include
includeSubDomains - Serve the header on the apex domain over HTTPS
- Submit at hstspreload.org
Getting on the list takes weeks to months. Getting off the list also takes weeks to months. Do not preload until you are certain your entire domain (all subdomains) can work exclusively over HTTPS.
How to add HSTS
Next.js (next.config.js)
// next.config.js
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains; preload',
},
],
},
];
},
};
module.exports = nextConfig;
Express
app.use((req, res, next) => {
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
next();
});
Or use the helmet middleware, which sets HSTS and several other security headers:
import helmet from 'helmet';
app.use(helmet());
// helmet enables HSTS with max-age=15552000 (180 days) by default
// For preload eligibility, override:
app.use(helmet.hsts({ maxAge: 31536000, includeSubDomains: true, preload: true }));
Nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
The always directive ensures the header is sent even on error responses (4xx, 5xx). Without it, nginx only sends custom headers on successful responses, which means an attacker who can trigger errors could prevent the HSTS header from being set.
Rails
# config/environments/production.rb
config.force_ssl = true
# Rails 7.1+ sets HSTS with max-age=31536000 by default when force_ssl is true
# To customize:
config.ssl_options = {
hsts: { subdomains: true, preload: true, expires: 1.year }
}
Vercel (vercel.json)
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Strict-Transport-Security",
"value": "max-age=31536000; includeSubDomains; preload"
}
]
}
]
}
Common mistakes
Setting max-age too low. A max-age of 300 (5 minutes) provides almost no protection. If the user does not revisit within 5 minutes, the protection expires. Start with 1 year.
Forgetting includeSubDomains. Without it, api.yourapp.com is not covered. An attacker can strip SSL on your API subdomain while your main domain is protected.
Adding preload before you are ready. The preload list is not easily reversible. If you have subdomains that do not support HTTPS (internal tools, legacy services), adding preload will break them. Audit all subdomains first.
Only setting it in the application. If your reverse proxy handles TLS termination and your application runs on HTTP internally, make sure the HSTS header is set at the proxy level, not in the application. The header must be sent over a valid HTTPS connection to be accepted by browsers.
24 out of 30
Twenty-four of the thirty projects in our benchmark do not configure HSTS. Some of them handle authentication. Some of them handle financial data. Some of them store files and documents.
The header is one line. It takes less time to add than it took to read this post. The attack it prevents is real, documented, and trivially executable on any shared network.
Add it today.