# HSTS header is present

- **Pattern:** `ab-002518` (`site-health-check.security-posture.hsts-header`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-20
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002518
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002518)

## Why it matters

Without HSTS, browsers will happily downgrade to HTTP on any request where the redirect is absent or stripped — a trivial attack on public Wi-Fi. HSTS instructs browsers to refuse HTTP connections entirely for the specified duration, eliminating the window where a man-in-the-middle can intercept the initial plaintext request. NIST SP 800-53 SC-8 and OWASP A02 (Cryptographic Failures) both flag the absence of HSTS as a transport security gap. A `max-age` below one year (31536000 seconds) leaves users exposed for shorter periods after each visit — insufficient to meaningfully protect repeat visitors. CWE-311 captures the failure: missing encryption of sensitive data in transit.

## Severity rationale

High because a missing or short-lived HSTS header leaves a downgrade window open on every first visit or after the max-age expires, enabling interception without any vulnerability in the application itself.

## Remediation

Add the `Strict-Transport-Security` header with a `max-age` of at least one year. In nginx:

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

In Next.js `next.config.js`:

```js
async headers() {
  return [{
    source: '/(.*)',
    headers: [{ key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' }]
  }];
}
```

Do not set HSTS on HTTP — it is silently ignored and only takes effect over HTTPS. Confirm deployment with `curl -sI https://yoursite.com | grep -i strict`.

## Detection

- **ID:** `hsts-header`
- **Severity:** `high`
- **What to look for:** Before evaluating, enumerate all security-related headers in the final response and extract the exact `Strict-Transport-Security` header value. Parse the `max-age` directive and convert it to an integer for comparison against the 31536000-second (1 year) threshold.
- **Detector snippet (shell-capable tools only):** If `curl` is available, fetch HEAD against `$BASE_URL` and inspect the response headers for `Strict-Transport-Security`. If the header is present and `max-age>=31536000`, pass. If missing or exit >=2 / command not found, fall back to prose reasoning.

  ```detect-curl
  curl -sS -I $BASE_URL/
  ```

- **Pass criteria:** `Strict-Transport-Security` header is present in the final response with a `max-age` value of at least 31536000 seconds (1 year). The header must appear in the actual HTTP response, not just in an HTML meta tag.
- **Fail criteria:** Header missing entirely, or `max-age` is present but the value is less than 31536000 seconds.
- **Skip (N/A) when:** The final URL uses `http://` (HSTS is only meaningful over HTTPS connections).
- **Detail on fail:** `"No Strict-Transport-Security header in response"` or `"HSTS max-age is 3600 — should be at least 31536000 (1 year)"`
- **Remediation:** HSTS tells browsers to always connect via HTTPS, preventing downgrade attacks. Configure it in your server or CDN:

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

  ```js
  // next.config.js headers()
  { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' }
  ```

  Verify after deploying with: `curl -sI https://yoursite.com | grep -i strict`

## External references

- cwe CWE-311
- owasp A02
- nist SC-8

Taxons: cryptography-and-secrets

HTML version: https://auditbuffet.com/patterns/ab-002518
