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

> What HSTS, Content Security Policy, X-Frame-Options, nosniff, Referrer-Policy and Permissions-Policy do, how to add them and run a security headers check.

URL: https://webintacta.com/en/guides/http-security-headers
Language: English (Spanish version: see the hreflang alternate of the HTML page)
Last updated: 2026-10-24

**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.


By Jose Soriano (WebIntacta) · published 2026-10-24

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.

## 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.

> Start with a short max-age (for example 300 seconds), check that everything works and raise it afterwards. A long HSTS with a subdomain that has no HTTPS leaves that subdomain unreachable for your visitors for the whole period.

### 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;
```

> The classic Nginx trap: if a location block has its own add_header, it stops inheriting the ones from the server block. The headers vanish exactly on those paths. Check several URLs, not just the home page.

### 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.

> Don't copy another site's headers blindly, including the ones in this example. A CSP with frame-ancestors 'none' stops your own page builder from loading the site in its preview, and a Permissions-Policy with payment=() can break an express checkout button. Every line has to make sense for your site.

### With the free scan

The [free scan](https://webintacta.com/en/free-website-security-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](https://webintacta.com/en/wordpress-maintenance) and in the [WordPress security](https://webintacta.com/en/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](https://webintacta.com/en/guides/llms-txt).

## Sources

- [OWASP HTTP Headers Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html)
- [MDN: Content-Security-Policy-Report-Only](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy-Report-Only)
- [MDN: Strict-Transport-Security](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security)
- [HSTS preload list requirements](https://hstspreload.org/)
- [Nginx: the add_header directive](https://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header)

## 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.

## About the author

[Jose Soriano](https://webintacta.com/en/about): Full-stack developer specialized in PrestaShop, WordPress and WooCommerce, who has passed the official PrestaShop course and exam. He has been building online stores and modules since 2023 and is the founder of WebIntacta, the website security service through which he scans, cleans and maintains small-business websites and online stores himself, remotely from Ibi (Alicante, Spain). WebIntacta facts: https://webintacta.com/en/about#datos