Docs
Add security headers
Add security headers
A few response headers tell the browser how to protect your visitors: which scripts it may run, whether it may guess a file's type, whether another site may load your page in a frame, and how much of the address it passes on when someone clicks a link. Without them, a single injected script or a hidden frame on someone else's site (clickjacking) works against your visitors exactly as the attacker planned.
The headers cost nothing to send and nobody notices them, until one of them stops an attack. The site checklist looks for the four basic ones.
What the checklist looks for
| Header | Safe value to start with | What it does |
|---|---|---|
Content-Security-Policy |
frame-ancestors 'self'; base-uri 'self'; object-src 'none' |
Limits what the page may load and who may frame it |
X-Content-Type-Options |
nosniff |
The browser must not treat a text file as a script |
X-Frame-Options |
SAMEORIGIN |
Only your own site may show the page in a frame |
Referrer-Policy |
strict-origin-when-cross-origin |
Other sites see only your domain, not the full address |
X-Frame-Options also counts as present when the Content-Security-Policy contains frame-ancestors, which is the modern replacement. Sending both does no harm.
Start Content-Security-Policy small
A full Content-Security-Policy lists every domain the page may load scripts, styles, fonts and images from. Get it wrong and the browser silently blocks Google Tag Manager, analytics, the cookie banner, chat widgets or payment forms. That is why the value in the table above is deliberately small: it controls framing, the <base> tag and old plugins (<object>), and leaves scripts alone. It cannot break GTM.
If you want a policy that also controls scripts, build it in two steps:
- Send it as
Content-Security-Policy-Report-Onlyfirst, next to the small enforcing header. The browser blocks nothing, but every violation appears in the developer console (F12 → Console). - Browse the site for a few days, including checkout, forms and pages with embeds, and add each domain you see to the policy. GTM is the hard part: every tag you add in GTM can load from a new domain, so a strict policy has to be updated with every new tag.
Only then switch the header name to Content-Security-Policy. The checklist counts only the enforcing header; Report-Only alone stays yellow.
Warning: Do not copy a strict CSP from another website. Its list of allowed domains is not yours, and the first thing it breaks is usually analytics or payments, where nobody looks until the numbers drop.
Check it yourself
Replace example.com with your site:
curl -sI https://example.com | grep -iE '^(content-security-policy|x-content-type-options|x-frame-options|referrer-policy):'
You should see all four lines (or three, if Content-Security-Policy contains frame-ancestors). A header that appears twice with different values usually means it is set in two places, for example in the server and in a plugin. Keep one.
How to fix
Pick the place you have access to. Headers set on the web server or the CDN cover every page and every file, including pages served from cache, so prefer those over the CMS.
WordPress
WordPress has no setting for these headers. Add them with the free Code Snippets plugin (Snippets → Add New, run everywhere) or at the end of your child theme's functions.php:
add_action('send_headers', function () {
header("Content-Security-Policy: frame-ancestors 'self'; base-uri 'self'; object-src 'none'");
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: SAMEORIGIN');
header('Referrer-Policy: strict-origin-when-cross-origin');
});
This runs only when WordPress itself builds the response. Caching plugins such as WP Rocket or LiteSpeed Cache often serve a stored copy of the page without starting PHP, and that copy goes out without the headers. If the check still shows them missing after you clear the cache, set them on the server or in Cloudflare instead (see those tabs).
cPanel
cPanel has no screen for response headers. Add them to the .htaccess file in the site's root folder (File Manager, with Show Hidden Files on), at the top of the file:
<IfModule mod_headers.c>
Header always set Content-Security-Policy "frame-ancestors 'self'; base-uri 'self'; object-src 'none'"
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"
</IfModule>
This works on Apache and on LiteSpeed, which reads .htaccess the same way. Save, then run the check. If nothing changed, the host has mod_headers disabled; ask support to enable it.
Plesk
Websites & Domains → example.com → Apache & nginx Settings, in Additional nginx directives add:
add_header Content-Security-Policy "frame-ancestors 'self'; base-uri 'self'; object-src 'none'" 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;
Click OK; Plesk checks the syntax and reloads nginx. If an application on the site (WordPress plugin, framework) already sends one of these headers, the response will carry it twice. Remove it in one of the two places.
nginx
Put the headers in the site's server { … } block: on Debian and Ubuntu in /etc/nginx/sites-available/example.com, on Rocky, Alma and CentOS in /etc/nginx/conf.d/example.com.conf:
server {
# …
add_header Content-Security-Policy "frame-ancestors 'self'; base-uri 'self'; object-src 'none'" 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;
}
always adds them to error pages and redirects too. Watch for one nginx trap: a location block that has its own add_header (for example for cache headers) drops all headers from the server level for the files it matches. Repeat the four lines in such a block, or keep them in a file and include it in both places.
sudo nginx -t && sudo systemctl reload nginx
Apache and LiteSpeed
With server access (Apache). Enable mod_headers (on Debian and Ubuntu: sudo a2enmod headers) and add the headers to the site's <VirtualHost *:443>: on Debian and Ubuntu in /etc/apache2/sites-available/example.com-le-ssl.conf or similar, on Rocky, Alma and CentOS in /etc/httpd/conf.d/:
Header always set Content-Security-Policy "frame-ancestors 'self'; base-uri 'self'; object-src 'none'"
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"
sudo apachectl configtest && sudo systemctl reload apache2
(httpd instead of apache2 on Rocky, Alma and CentOS.)
Without server access (shared hosting, LiteSpeed). Put the same four lines in the .htaccess in the site's root, wrapped in <IfModule mod_headers.c> … </IfModule> as in the cPanel tab.
Cloudflare
If the site runs behind Cloudflare (orange cloud), Cloudflare can add the headers on the free plan, for every page and file, whatever the origin sends:
- Open the domain and go to Rules → Transform Rules → Modify Response Header.
- Create rule, name it Security headers, apply it to All incoming requests.
- Add four entries with Set static: header name and value as in the table above (for example
X-Content-Type-Optionsandnosniff), then Deploy.
Cloudflare also has a managed transform called Add security headers. It sets X-Content-Type-Options, X-Frame-Options and a Referrer-Policy, but not Content-Security-Policy, so you still need the rule for that one.
Node.js and Next.js
Next.js: in next.config.js:
const securityHeaders = [
{ key: 'Content-Security-Policy', value: "frame-ancestors 'self'; base-uri 'self'; object-src 'none'" },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
];
module.exports = {
async headers() {
return [{ source: '/:path*', headers: securityHeaders }];
},
};
Express: a small middleware before your routes:
app.use((req, res, next) => {
res.set({
'Content-Security-Policy': "frame-ancestors 'self'; base-uri 'self'; object-src 'none'",
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'SAMEORIGIN',
'Referrer-Policy': 'strict-origin-when-cross-origin',
});
next();
});
The popular helmet package sets these and more in one line, but its default Content-Security-Policy is strict and blocks scripts from other domains, Google Tag Manager included. Configure its contentSecurityPolicy option before you rely on it.
Laravel
Create a middleware, for example app/Http/Middleware/SecurityHeaders.php:
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SecurityHeaders
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('Content-Security-Policy', "frame-ancestors 'self'; base-uri 'self'; object-src 'none'");
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
return $response;
}
}
Register it for every request in bootstrap/app.php (Laravel 11 and newer):
->withMiddleware(function (Middleware $middleware) {
$middleware->append(\App\Http\Middleware\SecurityHeaders::class);
})
Files in public/ (CSS, JavaScript, images) are served by the web server without Laravel, so they do not get these headers. For the checklist that is fine: it looks at the page.
IIS and ASP.NET
In the site's web.config:
<configuration>
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Content-Security-Policy" value="frame-ancestors 'self'; base-uri 'self'; object-src 'none'" />
<add name="X-Content-Type-Options" value="nosniff" />
<add name="X-Frame-Options" value="SAMEORIGIN" />
<add name="Referrer-Policy" value="strict-origin-when-cross-origin" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>
If the application also sets one of them in code, IIS sends both values. Keep one.
Hosted platforms
On Shopify, Wix, Squarespace and Webflow the platform sends the response headers and you cannot add your own. If the checklist reports a header missing there, there is nothing to change on your side; the item stays a warning and does not affect anything else.
When the page is meant to be framed
SAMEORIGIN and frame-ancestors 'self' stop every other site from showing your page in a frame. If a partner site, a booking widget or your own other domain embeds the page on purpose, list it: frame-ancestors 'self' https://example.org. Then drop X-Frame-Options: it cannot list other domains, and older browsers that read only it would block the embed.
Confirm the fix
Run the curl command from Check it yourself again, then open the target in PulseKeeper and click Check now on the Checklist tab. The item turns OK within a few minutes instead of at the next daily run. Open a few pages in the browser with the console open (F12) to make sure nothing is blocked.