Type something to search...
How to add HTTP security headers to WordPress?

How to add HTTP security headers to WordPress?

You can add HTTP security headers to WordPress in four main ways: with Header always set directives in your .htaccess file on Apache or LiteSpeed, with add_header directives in your Nginx configuration, with PHP using the send_headers action in a custom plugin, or with a plugin or CDN setting such as Cloudflare's response header rules. A solid starting set includes Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and Permissions-Policy, with a carefully tested Content-Security-Policy on top.

Security headers are instructions your server sends along with every page, telling browsers how to behave when they load your site. They help protect your visitors from clickjacking, content sniffing, data leakage, and some forms of cross-site scripting, and they take only a few minutes to add. This article explains what each important header does, shows how to add them with each method, and covers how to test the result without breaking your site.

What Are HTTP Security Headers?

Every time a browser requests a page, the server responds with the page content plus a set of HTTP headers. Some headers describe the content, like Content-Type. Security headers are a special group that ask the browser to enable protective features.

For example, a security header can tell the browser:

  • Only ever connect to this site over HTTPS.
  • Don't let other sites display this page inside a frame.
  • Don't guess a file's type if the server says something different.
  • Only run scripts that come from approved sources.

Browsers respect these instructions, which means you can block entire categories of attacks without changing a line of your theme or plugins. Security headers don't replace keeping WordPress updated or fixing vulnerable code, but they add a valuable layer of defence in the browser.

The Key Security Headers Explained

Strict-Transport-Security (HSTS)

HSTS tells browsers to only connect to your site over HTTPS for a set period of time, even if someone types http:// or clicks an old link. This prevents downgrade attacks where an attacker intercepts the insecure first request.

Strict-Transport-Security: max-age=31536000; includeSubDomains
  • max-age: How long, in seconds, the browser should remember the rule. 31536000 is one year.
  • includeSubDomains: Applies the rule to every subdomain as well.
  • preload: An optional flag used when submitting your domain to browsers' built-in HSTS preload list.

Only add HSTS once your entire site works correctly over HTTPS. Browsers cache the rule, so a mistake can't be undone instantly. Start with a short max-age such as 300 (five minutes), confirm everything works, then increase it. Only add includeSubDomains if every subdomain supports HTTPS, and only use preload if you're certain, since removal from the preload list can take a long time.

X-Content-Type-Options

This header stops browsers from "sniffing" a file to guess its type. Without it, a browser might treat an uploaded text file as executable JavaScript.

X-Content-Type-Options: nosniff

It has one valid value and almost never causes problems, so it's safe to add on every site.

X-Frame-Options and frame-ancestors

This header controls whether other sites can load your pages inside a frame. Blocking framing protects against clickjacking, where an attacker overlays your page with invisible elements to trick users into clicking something.

X-Frame-Options: SAMEORIGIN

SAMEORIGIN allows your own site to frame its pages, which WordPress needs for features like the Customizer preview, while blocking everyone else. WordPress already sends this header on the login and admin screens, but not on the front end.

The modern replacement is the frame-ancestors directive in Content-Security-Policy, which offers more control. Sending both is fine and gives the widest browser coverage.

Referrer-Policy

When a visitor clicks a link from your site to another site, the browser can send the full URL of the page they came from. That URL might contain sensitive information, like search terms or tokens in query strings.

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

This value sends the full URL for links within your own site, only your domain name to other HTTPS sites, and nothing when moving from HTTPS to HTTP. It's the default in modern browsers, but setting it explicitly ensures consistent behaviour.

Permissions-Policy

Permissions-Policy lets you disable powerful browser features your site doesn't use, such as the camera, microphone, or geolocation. If malicious code ever ends up on your page, it can't use features you've switched off.

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

An empty list, written as (), disables the feature for everyone. If your site genuinely uses a feature, for example a store locator that needs geolocation, allow it for your own origin with geolocation=(self).

Content-Security-Policy (CSP)

CSP is the most powerful security header, and the most complex. It tells the browser exactly which sources of scripts, styles, images, fonts, and frames are allowed. If an attacker manages to inject a <script> tag pointing to their own domain, a good CSP stops the browser from running it.

A CSP is made up of directives:

  • default-src: The fallback for any resource type you don't list separately.
  • script-src: Where JavaScript can load from.
  • style-src: Where stylesheets can load from.
  • img-src: Where images can load from.
  • frame-ancestors: Which sites can frame your pages.
  • object-src: Plugins like Flash. Set this to 'none'.
  • base-uri: Restricts the <base> tag, which attackers can abuse.

WordPress sites often rely on inline scripts and styles from core, themes, and plugins, plus external services like analytics, fonts, and embeds. A strict CSP that blocks all inline code would break many sites. That's why you should always start in report-only mode, which we'll cover below.

Headers You Can Skip

  • X-XSS-Protection: This controlled an old browser XSS filter that modern browsers have removed. Current guidance is to omit it or set it to 0, since the old filter could itself introduce problems.
  • Expect-CT: This header is deprecated and no longer needed.

Method 1: Add Security Headers With .htaccess (Apache and LiteSpeed)

If your host runs Apache or LiteSpeed, the .htaccess file in your WordPress root is the easiest place to add headers. They'll apply to every response, including images, CSS, and JavaScript files, not just WordPress pages.

  1. Back up your .htaccess file: Download a copy via SFTP or your host's file manager.

  2. Add the headers above the WordPress block: Paste the rules outside the # BEGIN WordPress and # END WordPress markers so WordPress doesn't overwrite them.

  3. Save and test: Reload your site and check the headers using the testing methods later in this article.

# Security headers
<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 Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"

    # Enable HSTS only after confirming the whole site works over HTTPS.
    # Start with a short max-age and increase it later.
    Header always set Strict-Transport-Security "max-age=300" "expr=%{HTTPS} == 'on'"
</IfModule>

The <IfModule mod_headers.c> wrapper prevents errors if the headers module isn't enabled. If headers don't appear, ask your host to enable mod_headers. The expr condition on HSTS ensures it's only sent over HTTPS connections, which is what the specification requires.

Once you're confident, raise the HSTS value to max-age=31536000.

Method 2: Add Security Headers With Nginx

On Nginx, add headers with the add_header directive in your site's server block, usually in /etc/nginx/sites-available/yourdomain.conf on Ubuntu and Debian or /etc/nginx/conf.d/ on RHEL-based systems:

server {
    listen 443 ssl;
    http2 on;
    server_name yourdomain.com;

    # ... ssl_certificate and other settings ...

    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=(), payment=()" always;
    add_header Strict-Transport-Security "max-age=300" always;

    # ... location blocks ...
}

The always parameter makes Nginx send the headers on error responses too, not just successful ones. Test and reload:

sudo nginx -t && sudo systemctl reload nginx

The Nginx Inheritance Gotcha

Nginx has an important quirk: add_header directives are only inherited from the server level into a location block if that location doesn't define any add_header of its own. Many WordPress configurations add caching headers for static files, like this:

location ~* \.(css|js|jpg|jpeg|png|gif|webp|svg|woff2)$ {
    expires 30d;
    add_header Cache-Control "public";
}

Because this block has its own add_header, none of your security headers are sent for those files. The common fix is to put your security headers in a separate snippet file, such as /etc/nginx/snippets/security-headers.conf, and include it in the server block and in every location that has its own add_header:

location ~* \.(css|js|jpg|jpeg|png|gif|webp|svg|woff2)$ {
    expires 30d;
    add_header Cache-Control "public";
    include snippets/security-headers.conf;
}

Method 3: Add Security Headers With PHP

If you can't edit server configuration files, you can send headers from WordPress itself. Put this code in a small custom plugin, such as wp-content/plugins/sajjad-security-headers/sajjad-security-headers.php, so it keeps working if you change themes:

<?php
/**
 * Plugin Name: Sajjad Security Headers
 * Description: Sends HTTP security headers on front-end WordPress responses.
 * Version:     1.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Send security headers on front-end requests.
 */
function sajjad_send_security_headers() {
if ( headers_sent() ) {
return;
}

header( 'X-Content-Type-Options: nosniff' );
header( 'X-Frame-Options: SAMEORIGIN' );
header( 'Referrer-Policy: strict-origin-when-cross-origin' );
header( 'Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()' );

if ( is_ssl() ) {
// Start low, then raise to 31536000 once confirmed.
header( 'Strict-Transport-Security: max-age=300' );
}
}
add_action( 'send_headers', 'sajjad_send_security_headers' );

The send_headers action fires when WordPress sends headers for front-end requests. Keep in mind the limitations of this approach:

  • Static files aren't covered: Images, CSS, and JavaScript files are served directly by the web server without loading WordPress, so they won't receive these headers.
  • Page caching can interfere: Some caching plugins and server caches store the page but not all of its headers. Clear your cache and test after adding the code.
  • Admin screens aren't included: send_headers doesn't fire in wp-admin. WordPress already sends X-Frame-Options on admin and login screens.

For these reasons, server-level configuration is usually the better choice when it's available.

Method 4: Add Security Headers With a Plugin

If you prefer a dashboard interface, several plugins can manage headers for you:

  • Headers Security Advanced & HSTS WP: A dedicated plugin that adds a recommended set of security headers with minimal configuration.
  • Really Simple Security (formerly Really Simple SSL): Offers security headers as part of its hardening features, with some advanced options in the paid version.
  • Solid Security and other security suites: Some include options for common headers alongside their other hardening settings.

Plugins are convenient, but check how they add headers. Some write rules to .htaccess, which works well on Apache, while others send headers from PHP, which has the same limitations as Method 3. Also make sure you aren't adding the same header twice from a plugin and your server configuration, which can produce duplicate or conflicting values.

Method 5: Add Security Headers at Your CDN

If your site sits behind Cloudflare or another CDN, you can add headers at the edge so they're applied to every response, including cached ones. In Cloudflare, this is done with Rules > Transform Rules using a response header modification rule, where you set each header name and value. Cloudflare also offers a managed transform that adds a basic set of security headers with a single toggle.

CDN-level headers are especially useful on managed hosting where you can't change the server configuration. Just avoid setting the same header in both the CDN and on your server with different values.

Adding a Content-Security-Policy Safely

CSP deserves its own careful process, because a wrong policy can break scripts, fonts, forms, analytics, or embeds. Here's a safe way to introduce one.

Step 1: Start in Report-Only Mode

The Content-Security-Policy-Report-Only header tells browsers to report violations without blocking anything. Your site keeps working while you see what would break.

<IfModule mod_headers.c>
    Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data: https:; connect-src 'self' https://www.google-analytics.com; frame-src https://www.youtube.com; frame-ancestors 'self'; object-src 'none'; base-uri 'self'; form-action 'self'"
</IfModule>

This example allows Google Tag Manager, Google Fonts, YouTube embeds, and inline code, which many WordPress sites need. Adjust it for the services your site actually uses.

Step 2: Review Violations

Open your site in a browser, then open the developer tools console. Any resource that would have been blocked appears as a CSP warning. Browse your key pages, submit forms, and test logged-in and logged-out views. Add legitimate sources to the right directive and repeat until the console is clean.

Step 3: Enforce the Policy

Once no legitimate resources are being reported, change the header name from Content-Security-Policy-Report-Only to Content-Security-Policy. Keep testing after plugin and theme updates, since new versions may load resources from new domains.

A Note on unsafe-inline

Allowing 'unsafe-inline' for scripts weakens CSP's protection against injected scripts. Removing it on WordPress usually requires nonces or hashes for every inline script, which is difficult with many themes and plugins. A policy that includes 'unsafe-inline' but restricts sources, framing, object-src, and base-uri is still a meaningful improvement over no policy at all. Treat stricter policies as a longer-term goal.

How to Test Your Security Headers

After adding headers, confirm they're actually being sent.

  • Use curl: Run a HEAD request and look for your headers in the output:
curl -sI https://yourdomain.com/ | grep -iE "strict-transport|x-content-type|x-frame|referrer-policy|permissions-policy|content-security"
  • Check a static file too: Run the same command against an image or CSS file URL to confirm headers apply beyond WordPress pages.

  • Use browser developer tools: Open the Network tab, click the main document request, and review the Response Headers section.

  • Use an online scanner: Tools like SecurityHeaders.com and the Mozilla HTTP Observatory grade your headers and explain what's missing.

  • Clear caches: If headers don't appear, purge your page cache, server cache, and CDN cache, then test again.


FAQ: HTTP Security Headers in WordPress

A good baseline is X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and Permissions-Policy, plus Strict-Transport-Security once your whole site runs on HTTPS. Add a Content-Security-Policy after testing it in report-only mode.

Most basic headers are very safe. Content-Security-Policy can break scripts, styles, or embeds if it's too strict, and a long HSTS max-age can cause problems if HTTPS isn't fully working. Test both carefully.

Common causes include page or CDN caching, the mod_headers module not being enabled on Apache, your server running Nginx and ignoring .htaccess, or Nginx location blocks overriding server-level add_header directives.

Server configuration is usually better because it covers every response, including images, CSS, and JavaScript, and it isn't affected by page caching. PHP is a good fallback when you can't edit server files.

Modern browsers prefer the frame-ancestors directive in CSP, but sending X-Frame-Options as well is harmless and helps older browsers. Using both is common practice.

No. The browser feature it controlled has been removed from modern browsers. Current guidance is to omit it or set it to 0, and rely on Content-Security-Policy instead.

Not directly. They protect your visitors and your site's reputation. HTTPS itself is a ranking signal, and HSTS helps enforce it, but headers are mainly a security measure.


Conclusion

HTTP security headers are one of the quickest ways to make a WordPress site safer for its visitors. A handful of lines in .htaccess, your Nginx configuration, a small custom plugin, or your CDN settings can protect against clickjacking, content sniffing, referrer leakage, and unwanted access to browser features, while HSTS keeps every connection on HTTPS.

Start with the low-risk headers, introduce HSTS gradually, and build your Content-Security-Policy in report-only mode before enforcing it. Test with curl and an online scanner after every change, remember to clear your caches, and revisit your policy when you add new plugins or services. With the right headers in place, the browser becomes an active partner in defending your site.

Share :

Related Posts

What are the best WordPress security plugins?

What are the best WordPress security plugins?

The best WordPress security plugins for most sites are Wordfence, Sucuri Security, Solid Security, MalCare, All-In-One Security (AIOS), Patchstack, a

Dive Deeper
What are the most common website security threats?

What are the most common website security threats?

The most common website security threats are vulnerable or outdated software, weak and stolen passwords, malware infections, injection attacks like S

Dive Deeper
What is the difference between posts and pages in WordPress?

What is the difference between posts and pages in WordPress?

The main difference between posts and pages in WordPress is that posts are timely, dated entries that appear in your blog feed, archives, and RSS fee

Dive Deeper