Skip to content
Issue docs

HSTS max-age shorter than one year

Standardhsts_max_age_too_shortIssue 170

What is this issue?

The page sends a Strict-Transport-Security (HSTS) header, but its max-age is shorter than one year (31,536,000 seconds).

HSTS tells a browser: "for the next max-age seconds, only ever connect to this site over HTTPS." The browser remembers that instruction, and refreshes it on every visit. A short max-age means the browser forgets quickly — anyone who has not visited for a day, an hour, or a few minutes is unprotected again.

For a page to pass this check:

  • max-age is at least 31536000.
Strict-Transport-Security: max-age=31536000; includeSubDomains

A short value is common during a careful rollout (start at 5 minutes, then a week, then a month). That is good practice — as long as the rollout is finished. This check reports a rollout that stopped part way.

When this check stays silent

  • Plain HTTP pages. Browsers ignore HSTS sent over HTTP (RFC 6797 §8.1), and the page is already reported as not served over HTTPS.
  • No usable policy. A missing header, a header with no max-age, and max-age=0 are all reported by "No Strict-Transport-Security header" (#41). This check only looks at policies that browsers would actually apply, so the two never describe the same header.

Why it matters

  • The protection lapses between visits. HSTS only helps a visitor whose browser still remembers the policy. With max-age=86400, a customer who comes back after two days types example.com, the browser sends a plain-HTTP request first, and that request can be intercepted and redirected (an "SSL stripping" attack) before the site's own redirect to HTTPS arrives.

  • One year is the accepted baseline. It is the minimum the browser preload list accepts, the value recommended by OWASP and Mozilla, and what security scanners and audits expect.

  • Trust signals. A site that only half-enables HSTS is easy to spot in security reviews and vendor questionnaires.

Effect on the health score

This is a standard issue. It deducts from the Delivery & Trust score for each affected page.

How to fix it

  1. Confirm the whole site works over HTTPS, including every page and asset, before committing browsers to HTTPS for a year.

  2. Raise max-age to at least one year. Two years (63072000) is also common and is what the preload list recommends.

    nginx

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    Apache

    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"

    Cloudflare: SSL/TLS → Edge Certificates → HTTP Strict Transport Security, set Max Age to 12 months.

  3. If you are mid-rollout, keep stepping up (one week, one month) but plan the final step to one year rather than leaving a short value in place.

  4. Check every layer. If the origin sends one value and the CDN another, the browser sees whichever arrives first. Look at the header a browser receives:

    curl -sI https://example.com | grep -i strict-transport

Examples

Example 1: A one-day policy

Strict-Transport-Security: max-age=86400; includeSubDomains

Reported: 86,400 seconds is one day.

Example 2: A one-year policy

Strict-Transport-Security: max-age=31536000

Passes this check. (It may still be reported for missing includeSubDomains, which is a separate suggestion.)

Example 3: A quoted, upper-case directive

Strict-Transport-Security: MAX-AGE="600"

Reported: directive names ignore case and values may be quoted, so this is read as a 10-minute policy.

Example 4: max-age=0

Strict-Transport-Security: max-age=0

Not this issue: max-age=0 switches HSTS off and is reported as "No Strict-Transport-Security header" instead.

How PixyScan detects this

  1. Only HTTPS pages are checked, because browsers ignore HSTS over HTTP.

  2. Skips anything #41 already reports. No header, no max-age, or max-age=0 are the "No Strict-Transport-Security header" issue.

  3. Parses the policy as described below.

  4. Reports when max-age is below 31,536,000 seconds. Exactly one year passes. The finding records the value found and the value required.

  5. Runs on every page, because the header is set per response. A CDN or proxy that sends a weaker policy on some paths than others shows up on exactly those pages.

How the header is read

PixyScan reads the header the way RFC 6797 tells a browser to:

  • Only the first header field counts. If the server sends the header twice, browsers use the first one and ignore the rest, and so does this check.
  • Directive names ignore case. includeSubDomains, INCLUDESUBDOMAINS and includesubdomains are the same directive.
  • Values may be quoted. max-age="31536000" is valid.
  • A repeated directive makes the header invalid. Browsers ignore a header like max-age=600; max-age=700 entirely, so this check says nothing about it rather than giving advice about a policy no browser applies.

What we store

Storage Level

Page Level


Database Table / Prisma Model

PageSecurityHeader, and the finding on audit_issues.details


Fields Used

Field Type Description
page_security_headers.hsts_header String The Strict-Transport-Security header exactly as the page sent it

Stored Fields on the finding

Field Type Description
message String What is missing from the policy and why it matters
hstsHeader String The header as sent
maxAge Int The max-age found, in seconds
requiredMaxAge Int 31536000, the value required

Detection Dependencies

  • HTTP Response headers
  • HTTPS (the check only applies to pages served over HTTPS)

Further reading