The HSTS max-age=0 Trap — When a Security Header Exists But Provides No Protection
The most common mistake when auditing security headers is stopping at "does the header exist?" For most headers, presence alone is enough. Strict-Transport-Security (HSTS) is the exception — its value can flip its meaning entirely, making it a textbook case where "it's there, so it's safe" turns out to be wrong. This guide covers why that happens and what an automated check needs to look at to catch it.
1. What the HSTS header actually does
HSTS is a response header like Strict-Transport-Security: max-age=31536000; includeSubDomains that tells the browser: "for the next max-age seconds, only ever connect to this domain over HTTPS." If a user accidentally types http://, the browser rewrites it to https:// on its own before the request ever leaves the machine, which shuts down man-in-the-middle downgrade attacks at the source. The max-age value is in seconds, and one year equals 31536000 seconds.
2. What max-age=0 actually means
This is where the trap begins. The HSTS spec, RFC 6797, defines max-age=0 not as "a short validity period" but as an explicit signal to immediately disable the HSTS policy. If a server sends Strict-Transport-Security: max-age=0, the browser stops treating that domain as HTTPS-only from that moment on. The header itself is clearly present in the response, but its actual protective effect is zero. In practice, this value isn't a typo — it's officially defined for cases where you deliberately want to turn HSTS off (for example, when one subdomain hasn't gotten its HTTPS certificate ready yet).
3. Why "the header is there" is a dangerous assumption
If a security scanning script or checklist only greps for the presence of the header, a response with max-age=0 gets misread as "HSTS applied: pass." In reality, that site has zero HTTPS-enforcement protection. This kind of false positive turns into a real incident in situations like these:
- Leftover config from a certificate incident: HSTS gets temporarily lowered to max-age=0 to allow an HTTP fallback while dealing with an expired or failed certificate renewal, and nobody remembers to revert it once the issue is fixed.
- Mid-migration on a subdomain: a parent domain using includeSubDomains gets dropped to max-age=0 because one subdomain isn't HTTPS-ready yet, and the change is left in place indefinitely.
- False negatives in automated scanners: any scanner that judges purely on "header present = pass" will keep missing this state entirely.
4. How to actually check the value
The HTTP Header Checker avoids this problem by parsing HSTS down to its max-age value instead of just checking presence. It extracts the number after max-age= with a regex, and only marks it "Present" if that number is greater than 0 — if max-age=0 or the max-age directive is missing entirely, it's classified as "Missing" regardless of whether the header line technically exists. Other security headers (CSP, X-Frame-Options, etc.) pass purely on presence regardless of their value's specifics; HSTS is the one exception that gets this special-cased logic.
| Actual response header | Header present? | Real protection | Correct verdict |
|---|---|---|---|
| Strict-Transport-Security: max-age=31536000 | Yes | HTTPS enforced for 1 year | Applied |
| Strict-Transport-Security: max-age=0 | Yes | None (disabled immediately) | Same as missing |
| (no header at all) | No | None | Missing |
5. Audit checklist
- Check the Strict-Transport-Security value directly with
curl -Ior your browser's DevTools, and make sure to actually read the number after max-age. - For production, max-age should be at least 6 months (15768000), ideally 1 year (31536000).
- Once whatever forced you to temporarily lower HSTS is resolved, add a step to your deployment checklist that restores the original max-age value.
- If the header is hardcoded in an Apache/Nginx config, regenerate the standard value (1 year) with the Apache Config Generator or Nginx Config Generator and diff it against what's live.
- Cross-check other security headers you might also be missing, such as CSP, with the CSP Generator.
Frequently Asked Questions
Q. Is there ever a legitimate reason to use max-age=0 on purpose?
Yes. It's the officially defined way in RFC 6797 to unwind an HTTPS migration or temporarily allow HTTP access while a subdomain's certificate issue gets sorted out. It should only ever be a temporary measure, though — once the underlying cause is fixed, the original value should be restored immediately.
Q. What about when the header exists but has no max-age value at all?
A Strict-Transport-Security header without a max-age directive is invalid per spec, and browsers ignore it. That case is functionally identical to HSTS not being applied at all.
Q. Do other security headers need value-level checking too?
For most headers like X-Frame-Options or X-Content-Type-Options, presence itself is essentially equivalent to protection. HSTS is unusual in that a single number, max-age, can flip it fully "on" or "off," which is why it specifically needs a separate value check.
Q. What should I watch out for when enabling HSTS for the first time?
If you turn on includeSubDomains, every subdomain needs to be HTTPS-ready. Any subdomain that isn't can become completely unreachable. It's safer to start with a short max-age for testing and gradually increase it.