Security headers and HSTS: a checklist for SEO teams
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
| Header | What it stops | A sensible start |
|---|---|---|
| Strict-Transport-Security | Visitors being sent to the insecure HTTP version, for example on public Wi-Fi | max-age=300 while testing, then max-age=31536000; add includeSubDomains once every subdomain works on HTTPS |
| Content-Security-Policy | Injected scripts running, by listing where scripts, styles and frames may come from | Start with Content-Security-Policy-Report-Only to see what would break, then enforce |
| X-Content-Type-Options | Browsers guessing a file's type and running it as something it isn't | nosniff |
| X-Frame-Options | Other sites loading your pages in a hidden frame to trick clicks ("clickjacking") | SAMEORIGIN, or frame-ancestors 'self' in the CSP |
| Referrer-Policy | How 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 it | strict-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:
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.
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:
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.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
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:// versions of your own pages. They work, through a redirect, but every one is a wasted hop. Update them in the template and content.http:// address, or a password field on an HTTP page. What visitors type is sent unencrypted, and browsers warn them.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
- Open DevTools, go to the Network tab and reload.
- Click the first request (the page itself) and open Headers.
- 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 Frog | Sitebulb | What it means |
|---|---|---|
| Security: HTTP URLs | Internal HTTP URLs | The page is served without HTTPS. |
| Security: Mixed Content | Mixed content (loads HTTP resources on HTTPS URL) | An HTTPS page loads a file over HTTP, which browsers may block. |
| No direct match | HTTPS URL links to an HTTP URL | A secure page links to an insecure URL. |
| Security: Form URL Insecure | HTTPS URL contains a form posting to HTTP | A form sends its data over HTTP. |
| Security: Missing HSTS Header | No direct match | The 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
srcsetimages) 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
Security headers are missing
| ✕ | Strict-Transport-Security | not sent |
| ✕ | Content-Security-Policy | not sent |
| ✓ | X-Content-Type-Options | nosniff |
| ✓ | X-Frame-Options | SAMEORIGIN |
| ✓ | Referrer-Policy | strict-origin-when-cross-origin |