All 30 checks Missing HSTS header

Missing HSTS header

Updated

Without a Strict-Transport-Security header, a browser still tries plain HTTP first whenever someone types your domain or follows an old http:// link, and that first unencrypted request is the one an attacker on the same network can tamper with.

Fails: HTTPS response headers, no HSTS
HTTP/2 200
content-type: text/html; charset=utf-8
cache-control: public, max-age=0, must-revalidate
x-content-type-options: nosniff
Fix: Header to add
Strict-Transport-Security: max-age=31536000; includeSubDomains

The fix for your stack

Fix for: Next.js / Vercel

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: "/:path*",
        headers: [{ key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains" }],
      },
    ];
  },
};

export default nextConfig;

What the browser does with HSTS, and without it

HTTP Strict Transport Security is a single response header. When a browser receives it over a valid HTTPS connection, it records the host and, for the number of seconds in max-age, refuses to talk to that host over plain HTTP. A typed example.com, a bookmark saved years ago as http://example.com, or a link in an old email all get rewritten to https:// inside the browser before any packet leaves the device. Certificate errors on that host also become hard failures the user cannot click through.

Without the header, the browser has no memory of your HTTPS setup. It sends the first request over HTTP, your server answers with a 301 to the HTTPS address, and only then does the encrypted session start. That redirect travels in cleartext. On a hostile network someone can answer in place of your server, keep the visitor on HTTP, and proxy the real site behind the scenes. The technique is old and well documented, and a redirect alone does not stop it.

Who is exposed

The people at risk are visitors on networks they do not control: hotel and airport Wi-Fi, conference networks, shared office connections. Anyone arriving through an http:// link is in the same position, and older sites tend to have plenty of those scattered across directories, printed material, and email signatures.

For an agency, the header is also a visibility problem. Client IT teams, insurers, and procurement questionnaires run header scanners, and a missing HSTS line is one of the first items they list. Handing over a site that fails it invites a support thread you could have avoided with one line of config.

How Skewcheck detects it

Skewcheck reads the response headers of the audited URL and looks for strict-transport-security. If the header is absent, the check fails with high severity and removes 6 points from the audit score, which starts at 100. If the header is present, the check passes. Skewcheck does not grade the max-age value, so choosing a sensible duration is still your job.

To see what Skewcheck sees, request the page yourself and filter the headers. Header names are case-insensitive, so the lowercase form shown by HTTP/2 is the same header.

Check the live response
curl -sI https://example.com | grep -i strict-transport-security

Adding the header on your platform

Set the header on the HTTPS side only. In nginx, the always flag makes it appear on error responses too, not just 200s. In Apache, mod_headers must be enabled (a2enmod headers on Debian-based systems). On Vercel, a headers entry in vercel.json applies to every path that matches the source pattern. In a Next.js app you can return it from the headers function in next.config. On Cloudflare, HSTS is a toggle in the edge certificate settings rather than something you write by hand. Some hosts already send a default HSTS header, so check the live response before adding a second one.

nginx (HTTPS server block)
server {
    listen 443 ssl;
    server_name example.com;

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

    # ssl_certificate, root, locations, etc.
}
Apache (HTTPS virtual host)
<VirtualHost *:443>
    ServerName example.com
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</VirtualHost>
vercel.json
{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "Strict-Transport-Security",
          "value": "max-age=31536000; includeSubDomains"
        }
      ]
    }
  ]
}
next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: "/:path*",
        headers: [
          {
            key: "Strict-Transport-Security",
            value: "max-age=31536000; includeSubDomains",
          },
        ],
      },
    ];
  },
};

export default nextConfig;

Mistakes that bite later

Browsers ignore Strict-Transport-Security when it arrives over plain HTTP, so adding it to the port 80 server block does nothing. In nginx, any add_header inside a location block replaces every add_header inherited from the server block, which quietly drops HSTS on exactly the paths that define their own headers.

includeSubDomains applies the policy to every subdomain, including the forgotten ones: a staging host without a certificate, an old webmail login, an internal dashboard on HTTP. Once a browser has the policy, those hosts stop loading for that visitor until max-age runs out. Audit your DNS first, or start with max-age=300 and raise it to a year once nothing breaks.

The preload directive is a separate commitment. Submitting a domain to the browser preload list builds the policy into browsers themselves, it requires includeSubDomains and a max-age of at least one year, and removal takes months to reach users. Leave preload off until the whole domain has been HTTPS-only for a while.

Before and after

Before
HTTP/2 200
content-type: text/html; charset=utf-8
x-content-type-options: nosniff
After
HTTP/2 200
content-type: text/html; charset=utf-8
x-content-type-options: nosniff
strict-transport-security: max-age=31536000; includeSubDomains

Check your page

Run all 30 checks on any URL. Free, no account.