Skip to content

HTTP security headers: what they are and how to check them

In short

The HTTP security headers that matter are six: Strict-Transport-Security (HSTS), Content-Security-Policy (CSP), X-Frame-Options, X-Content-Type-Options (nosniff), Referrer-Policy and Permissions-Policy. You add them in Apache, Nginx, WordPress or PrestaShop and check them with curl -I or with WebIntacta's free external scan. Start Content Security Policy in report-only mode so it doesn't break the site.

Security headers are a few lines your server sends with every page, telling the browser what it may and may not do: use HTTPS only, don't let another site load you in a frame, don't guess file types. They don't replace keeping the site updated, but they shut down whole classes of attacks for very little work.

By Jose SorianoLast updated: 7 min read

The six that matter

Strict-Transport-Security (HSTS)

It tells the browser to reach your site only over HTTPS for the time given, even if someone types http:// or clicks an old link. It stops an attacker on the same network (public Wi-Fi) from intercepting that first unencrypted visit.

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

max-age is in seconds (31536000 is one year). includeSubDomains extends it to every subdomain: only turn it on if all of them work over HTTPS, internal ones included. The preload directive is for getting onto the list browsers ship with; that requires a max-age of at least one year, includeSubDomains, an HTTP-to-HTTPS redirect and a valid certificate on every subdomain. Getting off that list takes months, so don't add it just to try it.

Content-Security-Policy (CSP)

It's the one that protects the most and the most delicate one. It defines which origins the page can load scripts, styles, images, fonts or frames from. If an attacker manages to inject a <script> pointing to their domain, a good CSP stops it from running. It's the defense in depth against XSS and against many card skimmers.

Content-Security-Policy: default-src 'self'; script-src 'self' https://www.googletagmanager.com; img-src 'self' data: https:; frame-ancestors 'self'; object-src 'none'; base-uri 'self'

The catch: a WordPress or PrestaShop site with ten plugins loads resources from many places, and a badly built CSP breaks the checkout, the chat or the analytics. Further down I explain how to roll it out without breaking anything.

X-Frame-Options and frame-ancestors

They stop another site from loading yours inside an invisible frame to trick users into clicking where they don't mean to (clickjacking). The modern way is the CSP frame-ancestors directive; X-Frame-Options is the old one, but it's still useful for old browsers and having both does no harm. OWASP recommends frame-ancestors if you can, and X-Frame-Options: DENY otherwise.

X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'

DENY forbids any frame; SAMEORIGIN allows only frames from your own domain (some visual editors and page builders need it). Watch out: frame-ancestors doesn't work if you put the CSP in a <meta> tag; it has to be sent as a header.

X-Content-Type-Options

With nosniff, the browser respects the file type the server declares and doesn't try to guess it. It stops a file uploaded as an image from running as a script. It has no downside: always send it.

X-Content-Type-Options: nosniff

Referrer-Policy

It controls how much information about the source page is sent when someone clicks a link to another site. With strict-origin-when-cross-origin, other domains only receive your domain, not the full URL (which may carry parameters with order or search data). It's the value OWASP recommends and the default in current browsers, but it's worth declaring.

Referrer-Policy: strict-origin-when-cross-origin

Permissions-Policy

It turns off browser features your site doesn't use: camera, microphone, geolocation… If an injected script tries to request them, the browser refuses. Empty parentheses mean "nobody".

Permissions-Policy: camera=(), microphone=(), geolocation=()

If your store uses a payment method that relies on the browser's Payment Request API, or your site has a map that asks for location, don't turn those features off blindly: test it.

The ones you don't need

X-XSS-Protection is obsolete: OWASP recommends not sending it or setting it to 0, because in old browsers it could create new vulnerabilities. And it's worth removing X-Powered-By and the versions in the Server header, which only tell an attacker what software you run.

How to add them

Apache (.htaccess)

On most shared hosting running Apache, they go in the .htaccess file at the root. It needs the mod_headers module, which almost every host has enabled:

<IfModule mod_headers.c>
  Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
  Header always set X-Content-Type-Options "nosniff"
  Header always set X-Frame-Options "SAMEORIGIN"
  Header always set Referrer-Policy "strict-origin-when-cross-origin"
  Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
  Header always unset X-Powered-By
</IfModule>

Nginx

In the site's server block. The always parameter makes them go out on error pages too:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

WordPress

The best place is the server (.htaccess or Nginx), because that way they also cover images and static files. If you don't have access, you can send them from PHP with the send_headers hook, in your own plugin or a mu-plugin (not in the theme's functions.php, which you lose if you switch themes):

<?php
add_action( 'send_headers', function () {
    header( 'X-Content-Type-Options: nosniff' );
    header( 'Referrer-Policy: strict-origin-when-cross-origin' );
    header( 'Permissions-Policy: camera=(), microphone=(), geolocation=()' );
    header( 'X-Frame-Options: SAMEORIGIN' );
} );

This way they only apply to the pages WordPress generates, not to the files the server delivers directly. WordPress already sends X-Frame-Options: SAMEORIGIN on its own on the login screen and in the dashboard.

PrestaShop

PrestaShop regenerates .htaccess when you save certain settings (friendly URLs, for example). It keeps whatever is outside the block marked by its # ~~start~~ and # ~~end~~ comments, so put your headers before # ~~start~~ or after # ~~end~~, never inside. If the store runs on Nginx, add them to the server configuration as shown above.

Content Security Policy without breaking the site

The safe way is to start in observation mode with Content-Security-Policy-Report-Only. The browser evaluates the policy and reports what it would have blocked, but blocks nothing:

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; frame-ancestors 'self'
  1. Publish the policy in Report-Only mode.
  2. Browse the site with the developer tools open (Console tab): every resource the policy would have blocked shows up as a warning. Above all, go through the cart, the checkout and the forms.
  3. Add the legitimate origins to the policy (payment gateway, analytics, chat). If you want to receive the reports from real visitors, set up an endpoint with report-to (and report-uri for browsers that don't support it yet).
  4. When days go by with no new warnings, rename the header to Content-Security-Policy.

Report-Only doesn't protect anything on its own: it only reports. Don't stay there forever. And if the site relies on lots of inline scripts, a CSP with 'unsafe-inline' in script-src protects much less; even so, frame-ancestors, object-src 'none' and base-uri 'self' help from day one.

How to check them

In the browser

Open your site, press F12, go to the Network tab, reload and click the first request (the document). Under Response Headers you'll see every header the server sends.

With curl

From a terminal on Windows, macOS or Linux:

curl -sI https://your-site.com/

For example, this site responds with (among others):

strict-transport-security: max-age=63072000; includeSubDomains; preload
content-security-policy: default-src 'self'; ... frame-ancestors 'none'; object-src 'none'; upgrade-insecure-requests
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()

Also check an inner page, an image and a URL that doesn't exist: that's where they're usually missing.

With the free scan

The free scan checks your home page's headers, the quality of the CSP (whether it's only in Report-Only mode, whether it allows scripts from anywhere) and HTTPS. If you need someone to set them up and make sure they don't break the checkout, that's included in WordPress maintenance and in the WordPress security review.

The same scan also reads your robots.txt and checks whether you publish an llms.txt, the file that summarizes your site for AI assistants. What it is, how it differs from robots.txt and how to write one is in the llms.txt guide.

Sources

Frequently asked questions

What are HTTP security headers?

They are lines the server sends with every response to tell the browser how to behave: use HTTPS only, which scripts it may load, whether the page may be shown inside a frame or which browser features are allowed.

How do I check a website's security headers?

In the browser, with F12 → Network → the first request → Response Headers. From a terminal, with curl -sI https://your-site.com/. WebIntacta's free scan checks them too.

Can a Content Security Policy break my site?

Yes, if it blocks a script you need, such as the payment gateway's. That's why you start with Content-Security-Policy-Report-Only, which only reports, and switch to the blocking version once no more warnings show up.

Is X-Frame-Options still needed?

The CSP frame-ancestors directive replaces it in current browsers, but X-Frame-Options still works for older ones and having both does no harm.

How do I add security headers in WordPress?

It's best done on the server, in .htaccess (Apache) or in the Nginx configuration. If you don't have access, a small plugin using the send_headers hook can send them from PHP.

Is your website intact? Check it now.

Free external scan. No sign-up and nothing to install.

Get the PDF result and 30-day alerts (optional)

Data protection. Controller: Jose Soriano Castro. Purpose: to send you the diagnostic PDF, email you for 30 days if a security flaw affecting your site is published (with a follow-up scan on day 30) and reply in writing to your question; offers only if you tick that box. Legal basis: your consent. Recipients: nobody, unless required by law. You can unsubscribe at any time with the link in every email or by writing to me. Rights: access, rectification, erasure, objection, restriction and portability. Privacy policy.

  • No FTP access
  • Read-only
  • No customer data

Cookie preferences