← Back to ClickJack Test

Apache Clickjacking Protection

Setting X-Frame-Options and Content-Security-Policy frame-ancestors in Apache httpd.

Quick config

Apache uses the Header directive from mod_headers. This module is enabled by default in most Apache distributions.

# Modern -- CSP frame-ancestors (all browsers since ~2018)
Header always set Content-Security-Policy "frame-ancestors 'none'"

# Fallback -- X-Frame-Options (older browser support)
Header always set X-Frame-Options "DENY"

One header is enough. Both is fine -- the browser uses CSP if it understands it, falls back to X-Frame-Options otherwise.

If you embed your own pages in iframes (dashboards, previews), replace 'none' with 'self' and DENY with SAMEORIGIN.

Where to put it

The Header directive works in three contexts. Pick the one you can access:

  • Virtual host block -- in your main Apache configuration (typically httpd.conf or /etc/apache2/sites-available/). This is the best place if you have access. Directives are loaded once at startup, not on every request.
  • .htaccess -- a per-directory configuration file. Useful on shared hosting, managed WordPress hosting, or when you do not have root access. Place it in your document root. Directives apply to that directory and all subdirectories.
  • <Directory> block -- limit the header to specific paths on your site.

.htaccess requirements

For the Header directive to work in .htaccess, the virtual host or directory must have AllowOverride FileInfo (or AllowOverride All). If you get a 500 Internal Server Error after adding the directive, AllowOverride is the likely culprit.

<VirtualHost *:443>
  # Required for .htaccess to override headers
  <Directory /var/www/html>
    AllowOverride FileInfo
  </Directory>
</VirtualHost>
Avoid .htaccess when you have server config access. Apache checks for .htaccess files in every parent directory on every request, even if none exist. This adds filesystem overhead. Put Header directives in your virtual host block instead -- they are loaded once and cached.

The always keyword

Apache has two internal header tables: onsuccess and always. The onsuccess table (the default when you omit the keyword) only applies to 2xx and 3xx responses. Error pages -- 403, 404, 500, 502, 503 -- get no headers.

# Wrong -- header missing on 404, 500, etc.
Header set X-Frame-Options "DENY"

# Right -- header on every response, including errors
Header always set X-Frame-Options "DENY"

This is the most common Apache clickjacking misconfiguration. Always use always for security headers.

The header table gotcha (duplicate headers)

A sharp edge worth knowing: always is not a superset of onsuccess. Apache stores headers for each table separately. If you set the same header in both tables -- or if a module like mod_proxy_fcgi writes to the always table while your config writes to onsuccess -- the response can contain duplicate headers.

# This can produce duplicate X-Frame-Options in some setups
Header set X-Frame-Options "DENY"
Header always set X-Frame-Options "DENY"

# Safer: use always, unset from onsuccess first
Header onsuccess unset X-Frame-Options
Header always set X-Frame-Options "DENY"

In practice, this mainly bites when you are proxying to a backend (PHP-FPM, Node.js, another Apache instance) that also sets these headers. If your backend sets headers, they land in the always table. A bare Header set in your config would go to onsuccess, and the response gets two copies.

The fix: pick one place (Apache or backend) and stick with it. If you want Apache to own security headers, strip the backend's copies:

# Strip backend headers, then set our own
Header always unset X-Frame-Options
Header always unset Content-Security-Policy
Header always set Content-Security-Policy "frame-ancestors 'none'"
Header always set X-Frame-Options "DENY"

Make sure mod_headers is loaded

On most distributions, mod_headers is enabled by default. If you get a syntax error when adding the Header directive, enable it:

# Debian / Ubuntu a2enmod headers systemctl reload apache2 # RHEL / CentOS / Fedora -- add or uncomment in httpd.conf: # LoadModule headers_module modules/mod_headers.so

Verify it is working

After reloading Apache, check that the header is present:

curl -I https://yoursite.com

Look for X-Frame-Options: DENY or Content-Security-Policy: frame-ancestors 'none' in the output.

Also test an error page -- a URL that returns 404 -- to confirm the always keyword is working:

curl -I https://yoursite.com/nonexistent

Then run it through ClickJack Test to confirm.

Notes on ALLOW-FROM

The X-Frame-Options: ALLOW-FROM https://example.com directive is obsolete. It was never supported in Chrome or Safari, and modern Firefox versions treat it the same as SAMEORIGIN. Use CSP frame-ancestors https://example.com if you need to allow framing from a specific origin.