HTTP security headers are short instructions your web server sends with every response, such as "only ever use HTTPS for this site", "do not let other sites put me in a frame" or "only run scripts from these places". Browsers follow them, so a few lines of server configuration close whole classes of attacks: clickjacking, MIME sniffing, many cross-site scripting cases and leaky referrers. This guide is for anyone who runs or pays for a website: what each header does in plain terms, the set that earns an A on any scanner, how to add them on your server or platform, and where to go deeper on each one.
Quick answer
The set that earns an A on a typical site:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Security-Policy: default-src 'self'; img-src 'self' data: https:; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()- Set them at the web server or CDN, not in HTML:
<meta http-equiv>works only for CSP and the referrer, with limits. - Get HTTPS right before HSTS, and start HSTS with a short
max-age. - Start CSP as
Content-Security-Policy-Report-Only, then enforce once nothing unexpected is reported. - Also set the Secure, HttpOnly and SameSite flags on cookies and hide version numbers in
ServerandX-Powered-By. - Run the security headers checker after each change.
What are HTTP security headers?
Every HTTP response starts with headers: name-value lines the browser reads before the page itself, such as Content-Type: text/html or Cache-Control: max-age=600. Security headers are the subset whose job is to tell the browser to be stricter than its defaults. You cannot see them on the page; they show in the browser's developer tools (Network → click the document → Response Headers) or with curl -sI https://example.com/.
Browsers are permissive by default because the web is old and must stay compatible: they will load your page inside any other site's iframe, guess a file's type from its content, run any script the HTML asks for and send the full previous URL to every site a visitor clicks through to. Each security header opts your site out of one of those behaviours. None of them changes how the site looks or works for a normal visitor, with one exception: Content-Security-Policy, which is why it gets its own rollout.
The OWASP Secure Headers Project and the MDN HTTP Observatory
Two public references define what "good" looks like. The OWASP Secure Headers Project keeps a list of the headers to send, with recommended values, and a list of headers to remove because they leak information or are obsolete. The MDN HTTP Observatory, run by Mozilla, grades a site's headers from A+ to F with published scoring rules. getReport's security headers checker grades against the Observatory rules, and the values in this guide agree with both.
Why security headers matter
Headers do not fix vulnerabilities; they limit the damage when something else goes wrong. A plugin with a cross-site scripting hole, a script injected through a compromised third party, a login page someone frames on a phishing site: with the right headers, the browser refuses to cooperate. They are defence in depth that costs a few lines and no ongoing work.
They also matter for trust. Security questionnaires from larger customers, cyber-insurance forms and penetration test reports all list missing headers, and a public scanner grade of F turns up in sales calls. The work to go from F to A is usually an afternoon.
What they do not do: headers are not a ranking factor, and Google has never said it uses them. The indirect link is real, though. A hacked site gets a Safe Browsing warning in Chrome and in search results and loses traffic within days, and headers make a successful attack less likely.
How to check for missing security headers
The checker requests your page like a browser, follows every redirect to the final response, and grades the headers against the MDN HTTP Observatory rules. It also checks every cookie the page sets for the Secure, HttpOnly and SameSite flags, and looks for version numbers leaking in Server and X-Powered-By.

It checks the one page you enter. Most servers send the same headers everywhere, but apps, CDNs and plugins can set different ones per path, so also check a login page, the shop and any subdomain. The fix pages for each finding are on security headers, HSTS, Content-Security-Policy, clickjacking, Referrer-Policy and Permissions-Policy.
The six headers every site should send
Strict-Transport-Security (HSTS)
Tells the browser to use HTTPS for your host for the next max-age seconds, without asking. After the first visit, even a typed http:// address or an old link goes straight to HTTPS, which closes the moment a network attacker could intercept the redirect. It also removes the "proceed anyway" button on certificate errors, which is why HTTPS has to work everywhere first.
Strict-Transport-Security: max-age=31536000; includeSubDomainsStart with max-age=86400 (a day), confirm every subdomain works over HTTPS, then raise it to a year. The staged rollout, what includeSubDomains really covers, and whether to join the browser preload list (hard to undo) are in HSTS: enabling it safely and joining the preload list.
Content-Security-Policy (CSP)
Lists where scripts, styles, images, fonts, frames and connections may come from. A script injected by a compromised plugin or a comment field does not run if its source is not on the list. It is the most powerful header and the only one likely to break things, because every analytics tag, font service, chat widget and embedded map must be on the list.
A policy most sites can start from, in report-only mode:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none''unsafe-inline' for styles is a common compromise, since inline style="" attributes are everywhere. For scripts it defeats the purpose, which is why the report warns about it; sites with inline scripts move to nonces or hashes.
Inventorying your sources, reading violation reports, choosing between nonces and hashes, and switching to enforcement without breaking checkout are in Content Security Policy: a safe rollout from report-only to enforced.
X-Content-Type-Options
nosniff stops the browser from guessing a file's type from its bytes. Without it, a file uploaded as an image that actually contains JavaScript can be run as a script. One value, no downside:
X-Content-Type-Options: nosniffX-Frame-Options and frame-ancestors
Both stop other sites from embedding your pages in an iframe, which is how clickjacking works: your real page loaded invisibly over a fake button, so the visitor's click lands on "confirm" or "delete". frame-ancestors in CSP is the modern control and accepts a list of allowed sites; X-Frame-Options covers older browsers. Send both:
X-Frame-Options: DENY
Content-Security-Policy: …; frame-ancestors 'none'Use SAMEORIGIN and frame-ancestors 'self' if your own site frames its pages, as some page builders' previews do. ALLOW-FROM is obsolete and ignored. Both headers, with server configs for nginx, Apache, Caddy, WordPress and Next.js, are in X-Frame-Options, frame-ancestors and nosniff: stop clickjacking.
Referrer-Policy
Controls what the Referer header reveals when a visitor follows a link or your page loads a third-party resource. Modern browsers already default to strict-origin-when-cross-origin; setting it explicitly covers older ones and documents the choice:
Referrer-Policy: strict-origin-when-cross-originLinks within your site get the full URL; other sites get only your origin (https://example.com/); nothing is sent from HTTPS to HTTP. Password-reset links and URLs with tokens in the query string stay private. What leaks without it and when a stricter value is worth it are in strict-origin-when-cross-origin and what the Referer header leaks.
Permissions-Policy
Turns off browser features your site does not use, for your page and everything embedded in it. A compromised third-party script then cannot ask for the camera, microphone or location:
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()Use geolocation=(self) if you use it yourself. Browsers ignore feature names they do not know, so listing more is harmless. Sensible defaults per type of site, next to the Referrer-Policy values, are in strict-origin-when-cross-origin, Referrer-Policy and Permissions-Policy.
Headers to remove or leave alone
The OWASP list of headers to remove matters as much as the list to add:
Server: Apache/2.4.41andX-Powered-By: PHP/7.4.3name your software and version, which tells an attacker which known flaws to try. Hide the version (server_tokens off;on nginx,ServerTokens Prodon Apache,expose_php = Offin php.ini). Keeping the software updated matters more.X-XSS-Protectionis obsolete; see the next section.Expect-CTis obsolete; Certificate Transparency is enforced by browsers without it.Public-Key-Pins(HPKP) is removed from browsers and could lock visitors out if misconfigured. Never send it.
X-XSS-Protection: obsolete, send 0 or leave it out
X-XSS-Protection switched on the XSS filter (the "XSS Auditor") built into Internet Explorer, Chrome and Safari. The filter tried to spot script from the URL being reflected into the page and block it. It was easy to get around, and attackers could also use it to switch off legitimate scripts or leak data across sites; MDN warns that the header can create cross-site scripting holes in otherwise safe sites.
Browsers removed the filter: legacy Edge in 2018, Chrome in version 78 (2019), and Safari in 15.4 (2022), per Chrome's and WebKit's release notes. Firefox never had one. In every current browser the header does nothing, so a value of 1; mode=block copied from an old guide protects nobody.
The OWASP HTTP Headers Cheat Sheet recommends either not sending it or sending X-XSS-Protection: 0, which switches the filter off in the old browsers that still have it. Remove it from your config, or replace the old value:
add_header X-XSS-Protection "0" always;The real protection against cross-site scripting is a Content-Security-Policy that blocks inline scripts. getReport does not grade X-XSS-Protection either way.
Cross-Origin headers: for web apps
The Cross-Origin headers (Cross-Origin-Opener-Policy, Cross-Origin-Embedder-Policy, Cross-Origin-Resource-Policy) are worth knowing about for web apps that handle sensitive data in pop-ups or need features like SharedArrayBuffer. A typical content or shop site does not need them, and getReport does not grade them. What each one does and its safe values are in Cross-Origin-Opener-Policy, COEP and CORP.
Beyond the six: cookies, CORS, SRI and exposed files
The same check covers the neighbours of the headers, because they fail for the same reason: defaults that trust too much.
Cookie flags: Secure, HttpOnly and SameSite
Cookies are set with a header too (Set-Cookie), and three attributes decide how exposed they are. Secure sends the cookie only over HTTPS. HttpOnly keeps it out of reach of JavaScript, so an injected script cannot steal a session. SameSite=Lax stops it being sent with most requests started by other sites, which blocks many cross-site request forgery attacks.
Session and login cookies need all three. Most platforms set them in one place, such as PHP's session settings or a framework's cookie options. Which cookies need which flag, and the setting in PHP, Express, nginx, Apache and WordPress, are in HttpOnly, Secure and SameSite cookie flags explained.
CORS and Access-Control-Allow-Origin
Cross-Origin Resource Sharing headers decide which other sites may read your responses with JavaScript. Access-Control-Allow-Origin: * is fine on fonts, public images and open data. It becomes a leak on anything that answers differently per visitor, such as an account API, because any site the visitor opens can read the answer.
When the wildcard is safe, when it is dangerous and how to write an allowlist are in Access-Control-Allow-Origin: what CORS allows and when * is dangerous.
Subresource Integrity for third-party scripts
An integrity attribute on a <script> tag pins the exact file you expect from a CDN; if the file changes, the browser refuses to run it. That protects against a compromised CDN, the kind of incident polyfill.io caused in 2024. It cannot protect scripts that change by design, such as tag managers and chat widgets.
When the hash helps, when it breaks the page, and when self-hosting the script is simpler are in Subresource Integrity for third-party scripts.
security.txt
Not a header but a file at /.well-known/security.txt that tells a researcher who finds a hole where to report it. It needs two fields, Contact and Expires, and the expiry date is the part people forget. The fields, a complete sample and hosting it on common platforms are in security.txt: the contact file for security researchers.
Directory listings and exposed files
The same misplaced trust shows in files the server hands out: an "Index of /" page, a readable .env with database passwords, a .git folder or a backup.zip in the web root. The report checks a few fixed paths for these. What leaks and the server rules that close it are in directory listings and exposed files.
How to add security headers
1. Get HTTPS right first
HSTS on a site with a broken subdomain locks visitors out of that subdomain. Before HSTS: every host has a valid certificate, http:// redirects to https:// in one hop, and no page loads http:// resources. The SSL/TLS guide covers all three.
2. Add the four safe headers
X-Content-Type-Options, X-Frame-Options, Referrer-Policy and Permissions-Policy break nothing on a normal site. Add them together.
nginx (in the server block, with always so error pages get them too):
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;Apache (.htaccess or the virtual host, with mod_headers enabled):
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"Caddy:
example.com {
header {
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"
-Server
}
reverse_proxy app:3000
}Cloudflare: Rules → Transform Rules → Modify Response Header, one rule per header, applied to all requests; or the managed transform that adds a common set.
WordPress without server access: a small must-use plugin or the theme's functions.php:
add_action('send_headers', function () {
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()');
});Headers set in PHP only cover pages PHP serves; static files and pages served from a page cache may not get them. The server config is better when you can reach it.
3. Add HSTS with a short max-age, then raise it
add_header Strict-Transport-Security "max-age=86400" always;Wait a day, check every subdomain over HTTPS, then set max-age=31536000; includeSubDomains.
4. Roll out CSP in report-only mode
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'" always;Open the site with the browser console visible: each report-only violation names a source to add (your analytics host, a font service, an embedded map). After a week with no unexpected reports, rename the header to Content-Security-Policy.
5. Fix cookies and leaks, then re-run the checker
Each change is one reload. The score ring goes up as findings pass; leaked versions and cookie flags are in the same panel and worth the extra ten minutes.
Platform guides
WordPress: the headers can live in four places: the web server, Cloudflare, the host's control panel or a ten-line must-use plugin. Which layer wins, and values that keep wp-admin, the block editor and page builders working, are in security headers in WordPress without a plugin.
nginx: the trap is inheritance. An add_header inside a location block replaces every header inherited from the server block, so a single cache rule for images can silently drop all your security headers on those files. A complete, commented server block with TLS, headers and caching is in nginx configuration for an A grade.
Apache and LiteSpeed shared hosting: .htaccess is usually the only file you can edit, and it works for headers as long as mod_headers is on. A tested file with headers, redirects and caching is in Apache .htaccess for an A grade.
Cloudflare in front of anything: headers set at the edge are added to what the origin sends, so avoid setting the same header in two places; two different X-Frame-Options values confuse browsers. HSTS has its own switch under SSL/TLS → Edge Certificates.
Netlify and Vercel: a _headers file in the publish directory on Netlify; headers in vercel.json or next.config.js on Vercel. See Netlify and Vercel: the headers, caching and redirects to set.
Shopify, Wix, Squarespace: the platform sets its own headers. You cannot add a CSP, but HTTPS, HSTS and framing protection are handled.
Verify
- The security headers checker shows every header finding passed and the score ring at 90 or more.
curl -sI https://example.com/ | grep -i -E "strict|content-security|x-frame|x-content|referrer|permissions"prints all six, from outside your server.- Check a second path too, such as an image or the login page, to catch nginx's inheritance trap.
- Nothing broke: forms submit, embedded videos play, checkout works. CSP problems show in the browser console as "Refused to load" or "Refused to execute".
Common mistakes
- HSTS with
includeSubDomainswhile a subdomain is still HTTP. Visitors cannot reach it untilmax-ageexpires. Check every subdomain first. - CSP enforced on day one. Something breaks, usually the tag manager or a font; start in report-only.
- Headers only in
.htaccesswhile Cloudflare or a cache serves the page. Check withcurlfrom outside, not from the server. - Setting headers in HTML meta tags. Only CSP and the referrer policy work that way, and a meta CSP cannot use
frame-ancestorsor report-only. - Duplicated headers from the server and a plugin, with different values. Keep one source.
X-Frame-Options: ALLOW-FROM. Obsolete and ignored; useframe-ancestorswith a list.- Still sending
X-XSS-Protection: 1; mode=block. Obsolete; remove it or send0.
Questions people ask
What are HTTP security headers?
HTTP security headers are response headers that tell the browser to be stricter than its defaults with your site. The main six are Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options, X-Frame-Options, Referrer-Policy and Permissions-Policy. They force HTTPS, limit where scripts load from, stop file-type guessing, block framing and limit what leaks to other sites. They are set once in the web server or CDN and cost nothing to run.
How do I add security headers to my website?
Add them where your responses are produced: the web server (nginx add_header, Apache Header always set), the CDN (Cloudflare Transform Rules), the host's control panel, or your framework's config. Start with X-Content-Type-Options, X-Frame-Options, Referrer-Policy and Permissions-Policy, which break nothing. Then add HSTS with a short max-age, and Content-Security-Policy in report-only mode. Check the result from outside with the checker or curl -sI.
What is HSTS?
HSTS (HTTP Strict Transport Security) is a response header that tells browsers to use only HTTPS for your site for a set time, for example Strict-Transport-Security: max-age=31536000; includeSubDomains for one year. After the first visit, the browser upgrades every http:// link itself, so an attacker on the network cannot intercept the redirect. Enable it only once every subdomain works over HTTPS, and start with a short max-age.
What does X-Frame-Options do?
X-Frame-Options tells browsers whether other sites may show your page inside a frame or iframe. DENY blocks all framing and SAMEORIGIN allows only your own site, which stops clickjacking: an attacker loading your page invisibly over a fake button. The modern replacement is the frame-ancestors directive in Content-Security-Policy, which accepts a list of allowed sites; send both for older browsers. ALLOW-FROM is obsolete and ignored.
How do I fix "HSTS missing from HTTPS server"?
Add the Strict-Transport-Security header to every HTTPS response at the web server or CDN. On nginx: add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; in the HTTPS server block. On Cloudflare, turn it on under SSL/TLS → Edge Certificates → HTTP Strict Transport Security. Start with max-age=86400, check every subdomain over HTTPS, then raise it to a year. The header is ignored over plain HTTP.
How do I enable HSTS in Apache?
Enable mod_headers (on Debian and Ubuntu, a2enmod headers and reload), then add Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" inside the <VirtualHost *:443> block, or in .htaccess if you cannot edit the server config. Put it only on the HTTPS host. Test with curl -sI https://example.com/ and look for the header; the Apache .htaccess guide has the full set.
How do I add security headers in nginx?
Add add_header lines to the server block with always, so they are sent on error pages too, for example add_header X-Content-Type-Options nosniff always;. Test with nginx -t and reload. Watch out: an add_header inside a location block replaces every header inherited from the server block, so either repeat them there or keep all of them in one place.
What are HTTP headers?
HTTP headers are name-value lines sent before the content of every request and response. The browser sends headers such as the user agent and accepted languages; the server answers with headers such as the status, content type, caching rules, cookies and security instructions. You cannot see them on the page; open the browser's developer tools → Network → click the document, or run curl -sI https://example.com/.
Is X-XSS-Protection still needed?
No. X-XSS-Protection switched on a cross-site scripting filter that Chrome, Edge and Safari have since removed and Firefox never had, and the filter itself could be abused to leak data. Remove the header, or send X-XSS-Protection: 0 to switch the filter off in old browsers, as the OWASP Secure Headers Project recommends. Protect against cross-site scripting with a Content-Security-Policy instead.