Security headers and HSTS: a checklist for SEO teams

Short answer

Security headers are lines the server sends with every page that tell the browser how to protect visitors. The five worth checking are Strict-Transport-Security (HSTS), Content-Security-Policy, X-Content-Type-Options, X-Frame-Options and Referrer-Policy. They're set once at the server or CDN (content delivery network), not page by page. Check them in DevTools or with the snippet below, and treat HSTS with care: a wrong setting can lock people out of parts of the site.

Why SEO teams should care

Security headers don't boost rankings. Google lists HTTPS under page experience, but says page experience aspects other than Core Web Vitals don't directly help a site rank. So why put them in an SEO audit?

  • They're cheap to check and easy to miss. An SEO audit is often the only regular look anyone takes at the response headers.
  • HTTPS problems are visible. Browsers label HTTP pages "Not secure", and mixed content can break how a page looks and works.
  • Migrations leave a trail. Old http:// links, canonicals and hreflang URLs survive a move to HTTPS for years, and each one costs a redirect.

The five headers

HeaderWhat it stopsA sensible start
Strict-Transport-SecurityVisitors being sent to the insecure HTTP version, for example on public Wi-Fimax-age=300 while testing, then max-age=31536000; add includeSubDomains once every subdomain works on HTTPS
Content-Security-PolicyInjected scripts running, by listing where scripts, styles and frames may come fromStart with Content-Security-Policy-Report-Only to see what would break, then enforce
X-Content-Type-OptionsBrowsers guessing a file's type and running it as something it isn'tnosniff
X-Frame-OptionsOther sites loading your pages in a hidden frame to trick clicks ("clickjacking")SAMEORIGIN, or frame-ancestors 'self' in the CSP
Referrer-PolicyHow much of the page address goes to other sites. Browsers already default to sending only the domain; the header makes that explicit and stops plugins loosening itstrict-origin-when-cross-origin (the browser default)

Content-Security-Policy is the one to take slowly. A strict policy written in a hurry blocks your own analytics, tag manager and chat widget. The report-only version shows what would be blocked without blocking it.

What HSTS does, and where it goes wrong

HSTS tells the browser: "for the next max-age seconds, only ever use HTTPS for this site". After the first secure visit, the browser upgrades any http:// address on that site itself, before a request leaves the device:

Without HSTS an http:// link, or an older browser
Visitor types example.com
GET http://example.com/
301 → https://example.com/
GET https://example.com/  200

The first request goes out unencrypted,
and can be intercepted. Browsers that try
HTTPS first fall back to HTTP if it fails.
With HSTS after the first visit
Visitor types example.com
Browser upgrades it before sending
GET https://example.com/  200

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

Things to know before switching it on:

HTTPS only Browsers ignore the header if it arrives over plain HTTP. Send it on HTTPS responses, and redirect HTTP to HTTPS separately.
First visit It only works after the browser has had one secure response from the site, so the redirect still matters.
includeSubDomains Covers every subdomain of the host that sends it. Sent from example.com, it covers intranet.example.com too; sent only from www.example.com, it doesn't. An http://-only subdomain then stops working for visitors who have the rule. Check them all first.
No way past errors With HSTS on, a certificate problem gives visitors no "proceed anyway" option. An expired certificate takes the site down for them.
preload Lets you submit the domain at hstspreload.org so browsers ship with the rule built in, and even first visits use HTTPS. It needs the header on the bare domain with a max-age of at least a year, includeSubDomains and preload, and every subdomain on HTTPS. Removal takes months to reach users, so only add preload when you're sure.

A safe route is to start with a short max-age, such as 300 seconds, check nothing breaks, then raise it to a year.

HTTPS leftovers

Mixed content An HTTPS page that loads scripts, styles, frames or images over http://. Browsers block scripts, styles, frames and responsive images (srcset) outright, and try to upgrade plain images and media, so parts of the page can quietly fail.
HTTP links Internal links to http:// versions of your own pages. They work, through a redirect, but every one is a wasted hop. Update them in the template and content.
Insecure forms A form that posts to an http:// address, or a password field on an HTTP page. What visitors type is sent unencrypted, and browsers warn them.
Canonicals, hreflang and sitemaps Check these name https:// URLs too. See canonical tags and hreflang.

How to check by hand

1Run the security snippet 30 seconds

Open DevTools (F12), go to the Console and paste:

// Checks this page's security headers, then mixed content, HTTP links and insecure forms
const res = await fetch(location.href, { cache: 'no-store' });
const h = name => res.headers.get(name) || '';
const https = location.protocol === 'https:';
const csp = h('content-security-policy');
const xcto = h('x-content-type-options');
const row = (header, ok, sent, problem = 'MISSING') => ({ header, sent: sent || '(not sent)', result: ok ? 'ok' : sent ? problem : 'MISSING' });
console.table([
  row('HTTPS', https, location.protocol, 'NOT HTTPS'),
  ...(https ? [row('Strict-Transport-Security', !!h('strict-transport-security'), h('strict-transport-security'))] : []),
  row('Content-Security-Policy', !!csp, csp.slice(0, 80)),
  row('X-Content-Type-Options', xcto.toLowerCase() === 'nosniff', xcto, 'should be nosniff'),
  row('X-Frame-Options or frame-ancestors', !!h('x-frame-options') || /frame-ancestors/i.test(csp),
    h('x-frame-options') || (/frame-ancestors/i.test(csp) ? 'CSP frame-ancestors' : '')),
  row('Referrer-Policy', !!h('referrer-policy'), h('referrer-policy')),
]);
const site = location.hostname.replace(/^www\./, '');
const isHttp = url => /^http:\/\//i.test(url);
const srcsetUrls = el => (el.getAttribute('srcset') || '').split(',').map(s => s.trim().split(/\s+/)[0]).filter(Boolean)
  .map(u => { try { return new URL(u, document.baseURI).href; } catch { return ''; } });
const mixed = https ? [...new Set([
  ...[...document.querySelectorAll('script[src], link[rel~="stylesheet" i][href], img[src], iframe[src], video[src], audio[src], source[src], embed[src]')]
    .map(el => el.src || el.href),
  ...[...document.querySelectorAll('img[srcset], source[srcset]')].flatMap(srcsetUrls),
  ...[...document.querySelectorAll('object[data]')].map(el => el.data),
])].filter(isHttp) : [];
const httpLinks = https ? [...document.querySelectorAll('a[href]')]
  .filter(a => a.protocol === 'http:' && a.hostname.replace(/^www\./, '') === site).map(a => a.href) : [];
// getAttribute, because form.action returns a child element if a field is named "action"
const actionOf = f => { try { return new URL(f.getAttribute('action') || location.href, document.baseURI).href; } catch { return ''; } };
const forms = [...document.forms]
  .filter(f => (https && isHttp(actionOf(f))) || (!https && f.querySelector('input[type="password" i]')))
  .map(actionOf);
console.log('Mixed content:', mixed.length ? mixed : 'none');
console.log('Links to the HTTP version of this site:', httpLinks.length ? httpLinks : 'none');
console.log('Forms that send data unencrypted:', forms.length ? forms : 'none');

It lists each header with what the server sent, then any mixed content, links to the HTTP version of the site, and forms that send data unencrypted.

It reads the headers of the page as your browser gets it. A CDN can send different headers to different visitors, so check a couple of page types. A Content-Security-Policy-Report-Only header doesn't count as a policy here, because it doesn't block anything. It only sends reports if it has a report-to or report-uri directive; otherwise watch the Console.

2Read the headers in DevTools 1 minute

  1. Open DevTools, go to the Network tab and reload.
  2. Click the first request (the page itself) and open Headers.
  3. Look under Response Headers for the five names above.

The Console also prints a warning for each piece of mixed content the browser blocked or upgraded.

3Test the HTTP version 1 minute

Type the site's address with http:// in a private window. It should land on the https:// version in a single redirect. Once HSTS is cached, DevTools shows a "307 Internal Redirect" instead of the server's answer: curl -I http://example.com in a terminal shows the real one.

How crawlers report this

If a crawl sent you here, these are the labels to look for. Our guide to Screaming Frog and Sitebulb issues covers other common ones.

Screaming FrogSitebulbWhat it means
Security: HTTP URLsInternal HTTP URLsThe page is served without HTTPS.
Security: Mixed ContentMixed content (loads HTTP resources on HTTPS URL)An HTTPS page loads a file over HTTP, which browsers may block.
No direct matchHTTPS URL links to an HTTP URLA secure page links to an insecure URL.
Security: Form URL InsecureHTTPS URL contains a form posting to HTTPA form sends its data over HTTP.
Security: Missing HSTS HeaderNo direct matchThe server doesn't tell browsers to always use HTTPS.

Doing this on every page?

SEOSnip checks security for you

Open the extension on any page and it reads the response headers and the page's resources, links and forms. You get:

  • A checklist of the five headers, with the value the server sent for each. A missing Content-Security-Policy or Referrer-Policy on its own doesn't raise a ticket: the first is a project of its own, and browsers already default the second
  • Pages served over HTTP, and HTTP scripts, styles, images (including responsive srcset images) and frames on HTTPS pages
  • Internal links to http:// URLs, and forms that send data unencrypted
  • A ticket with that evidence, and a "Check if fixed" button that re-runs the check