How to Add Security Headers on WordPress

You can do this

Self-hosted WordPress gives you full control over your response headers. The catch is choosing the right layer to set them in, and not setting them twice. Here is how to do it properly.

This applies to self-hosted WordPress, including WooCommerce stores. You control the web server, so you control the headers.

The short answer

Set your security headers in your web server config. That means an Apache vhost or .htaccess file, or an nginx server block. This is the only method that covers every response your site sends, including images, CSS and JavaScript.

There are two other ways to do it, PHP and plugins, and both have real limits. We will get to those. But the server config is the one we would reach for first.

The headers worth setting

Five headers do most of the work. You do not need to understand all of them to benefit from them.

Strict-Transport-Security

Tells browsers to only ever reach your site over HTTPS. Read more about HSTS before you turn it on, because it is hard to undo.

Content-Security-Policy

Controls what scripts and resources are allowed to load. The most powerful header, and the easiest one to break your site with. See CSP.

X-Frame-Options

Stops other sites putting your pages in an iframe, which is how clickjacking works. SAMEORIGIN is the usual choice.

X-Content-Type-Options

Set to nosniff. Stops browsers guessing at file types. Cheap to add, no downside, and commonly missing.

Referrer-Policy

Controls how much of your URL gets sent to other sites when a visitor clicks a link away from you.

Apache: the .htaccess or vhost method

If your host runs Apache, and most cPanel hosts do, add this to your vhost config or to the .htaccess file in your WordPress root. Put it outside the # BEGIN WordPress block, because WordPress rewrites that section when it updates rules.

<IfModule mod_headers.c>
  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 Strict-Transport-Security "max-age=31536000; includeSubDomains"
</IfModule>

The word always is not decoration. Without it, Apache skips the header on error pages and redirects, so your 404s and your HTTP to HTTPS redirect go out unprotected. Use Header always set, every time.

Two things Apache needs first

If you paste that in and nothing happens, it is almost always one of these.

  • mod_headers must be enabled. The <IfModule> wrapper above means Apache silently skips the whole block if the module is missing, rather than throwing a 500. Handy, but it also hides the problem. Ask your host if you are not sure.
  • AllowOverride FileInfo must be on for the directory, otherwise Apache ignores Header directives in .htaccess entirely. On shared hosting this is usually already set. On your own server, check the vhost.
  • Only add HSTS once HTTPS works everywhere. Certificate first, then force HTTPS, then HSTS. Never the other way around.

nginx: the server block method

If you are on nginx, the equivalent goes in your server block. The always parameter does the same job as it does in Apache, covering 4xx and 5xx responses.

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 Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

nginx has a trap here worth knowing. add_header directives are only inherited from the level above if the current level sets no add_header of its own. So a single add_header inside one location block silently throws away every security header from your server block. If you set a header in a location block, repeat them all there, or pull them into an include snippet you reference in every block.

The PHP method, and why it is not enough

You can set headers from your theme or a small plugin using the send_headers hook. It works, and it is portable across hosts. But it only covers responses that PHP renders.

add_action( 'send_headers', function () {
    header( 'X-Content-Type-Options: nosniff' );
    header( 'X-Frame-Options: SAMEORIGIN' );
    header( 'Referrer-Policy: strict-origin-when-cross-origin' );
} );

Static assets never touch PHP. Your images, CSS and JavaScript are served straight off disk by Apache or nginx, so none of these headers reach them. This is why a scanner can pass your homepage while most of your files stay unheadered.

The mistake almost everyone makes

Pick one layer and stick to it. If you set the same header in your vhost and in .htaccess, or in a plugin and in the server config, you end up sending it twice.

For most headers a duplicate is just untidy. For CSP it is a real problem. When a browser sees two Content-Security-Policy headers it applies the intersection of both, meaning the most restrictive combination of the two. That is very rarely what you intended, and it produces the kind of bug where a page works fine until one specific script silently refuses to load.

So before you add headers anywhere, check what your site already sends. Our free safety check will show you.

About CSP and HSTS specifically

These two deserve caution, and most guides skip past that.

CSP will break your site if you rush it. A strict policy blocks anything you did not explicitly allow, and a typical WordPress site loads scripts from a dozen places you have forgotten about: analytics, chat widgets, payment providers, page builders. Start with Content-Security-Policy-Report-Only, watch what it would have blocked, and only then enforce it. On a WooCommerce store, test checkout properly before you switch it on.

HSTS preload is close to permanent. Getting on the preload list needs a max-age of at least 31536000, plus includeSubDomains and preload, a valid certificate, an HTTP to HTTPS redirect on the same host, and every single subdomain on HTTPS. Removal is not quick, it takes months to reach users through browser updates. Plain HSTS without preload is a fine idea. Preload is a commitment, so only do it if you are sure.

What about plugins

Plugins that set security headers exist, and one may suit you if you have no access to your server config. We are not naming one here, because the popular choice in this category changes often and we would rather not point you at something abandoned.

If you do use one, the rule above still applies: use the plugin or the server config, not both. And remember a plugin runs in PHP, so it inherits the same static-asset gap described above.

Check your work in seconds

Run your site through our free safety check to confirm the fix is live, and see what else a shopper would notice.

Run a free check

Frequently asked questions

Should I add security headers in .htaccess or functions.php?

Use .htaccess or your server config if you can. It covers every response, including images, CSS and JavaScript, which are served without PHP ever running. The functions.php approach using the send_headers hook only covers pages PHP renders, so most of your files stay uncovered. Use PHP only if you genuinely cannot edit the server config.

Why is my Header set line in .htaccess doing nothing?

Three usual causes. mod_headers may not be enabled, and if you wrapped the rules in an IfModule block Apache will skip them silently rather than error. AllowOverride FileInfo may not be set for the directory, which makes Apache ignore Header directives in .htaccess. Or your host serves the site through nginx, in which case .htaccess is not read at all.

What does always mean in Header always set?

Without always, Apache only adds the header to successful responses. Error pages and redirects go out without it. That includes your HTTP to HTTPS redirect and every 404, so a scanner can easily report the header as missing. Always use Header always set. On nginx the equivalent is adding the always parameter to add_header.

Do these steps work for WooCommerce?

Yes. WooCommerce is WordPress, so it is the same server config and the same rules. The one extra caution is Content-Security-Policy. Payment providers and fraud tools load scripts from their own domains, so test the full checkout flow in report-only mode before enforcing a policy.

Can I set the same header in two places to be safe?

No, and it can hurt. Duplicate headers are messy in general, and with Content-Security-Policy browsers apply the intersection of every policy they receive, meaning the most restrictive combination. That usually blocks something you meant to allow. Choose one layer and keep all your headers there.

Is HSTS safe to turn on?

Plain HSTS is fine once HTTPS works across your whole site, and you should get your certificate sorted first. Preloading is a different matter. It requires every subdomain on HTTPS, and getting removed from the preload list takes months to reach real users. Do not preload casually.

Security headers on other platforms

Other fixes for WordPress

See the full fix-it matrix →

Prove your site is safe.

Once it is fixed, show it. Get a badge your shoppers can verify, backed by continuous checks. Free to start.

Get started free Run a free check