Skip to main content
Average reading time: 3 minutes, 42 seconds
Please Rate
Tutorial Cover Decorative image

A Content Security Policy is one of the strongest defenses against XSS, but a wrong policy can blank your pages or lock you out of the admin. Joomla ships everything you need in the core System - HTTP Headers plugin.

This guide sets the baseline security headers first, then rolls out a CSP in report-only mode, tightens it with a nonce, and only then enforces it.

Work safely: apply every CSP change to the Site client only until it is proven, and keep a second logged-in admin session open in another browser while you test.

Prerequisites

  • Joomla 5.x or 6.x running over HTTPS
  • Super User access to the Plugin Manager
  • Your browser's developer tools (Console and Network tabs)

1. Enable the plugin

  1. Go to System → Manage → Plugins.
  2. Search for HTTP Headers and open System - HTTP Headers.
  3. Set Status to Enabled, but don't save yet.

2. Set the baseline security headers

On the plugin's main tab, set these values. They are safe for almost every site:

Header Value Why
X-Frame-Options SAMEORIGIN Stops other sites framing yours (clickjacking)
Referrer-Policy strict-origin-when-cross-origin Sends only your origin to other sites, never full URLs
Cross-Origin-Opener-Policy same-origin Isolates your pages from cross-origin popups

HSTS: start small

Strict-Transport-Security tells browsers to use HTTPS only. Enable it with a short max-age such as 300 seconds, confirm nothing breaks, then raise it to 31536000 (one year).

Important: only tick Include subdomains if every subdomain serves HTTPS, and don't enable Preload unless you understand that removal from browser preload lists takes months.

Save the plugin.

3. Avoid duplicate headers

If the same header is also set in .htaccess, the server config, Cloudflare or a security extension such as Admin Tools, browsers receive it twice and may pick the stricter one or ignore both. Decide on one place per header. To see what is actually sent, run:

curl -sI https://example.com/ | grep -iE "content-security|x-frame|referrer|strict-transport|cross-origin"

4. Start the CSP in Report-Only mode

Open the Content-Security-Policy (CSP) tab and set:

  • Content-Security-Policy (CSP): Enabled
  • Report-Only: Yes. Browsers log violations but block nothing.
  • frame-ancestors self: Yes

Then add one row per directive in the subform. Set Client to Site on every row:

Policy Directive Value
default-src 'self'
script-src 'self'
style-src 'self' 'unsafe-inline'
img-src 'self' data:
font-src 'self'
object-src 'none'
base-uri 'self'

Save, then reload your site. The response should now contain a Content-Security-Policy-Report-Only header.

5. Collect the violations

Browse the whole front end: home page, a blog layout, an article, forms, search, and any page with maps or video. Watch the browser Console. Each blocked resource appears as a CSP violation naming the directive and the URL.

Joomla has no built-in report collector, so the Console is your main source. For a busy site, add a report-uri directive that points to an external CSP reporting service, and let it gather reports from real visitors for a few days.

6. Allow only what you actually use

Add each legitimate source to the matching directive. For example, a site using Google Fonts and GA4 would need:

Directive Add
style-src https://fonts.googleapis.com
font-src https://fonts.gstatic.com
script-src https://www.googletagmanager.com
connect-src https://*.google-analytics.com

Repeat steps 5 and 6 until a full walk through the site produces no new violations.

7. Tighten scripts with a nonce

Enable Nonce on the CSP tab. Joomla then adds a random nonce attribute to the scripts it outputs. The header only uses it if you add the {nonce} placeholder to your script-src value:

script-src  'self' {nonce}

Optionally enable strict-dynamic, so scripts loaded by a nonced script are trusted too. Inline scripts printed by some third-party extensions may not receive the nonce. Test every page again in Report-Only before moving on.

8. Enforce the policy

When the Console stays clean, set Report-Only to No and save. Browse the site again from a private window, logged out and logged in.

9. Then, carefully, the administrator

The admin area loads more inline code and editor resources. Add separate rows with Client set to Administrator, and roll them out with the same Report-Only routine. Test the article editor, media manager and extension updates before enforcing.

Verify it works

  • Run the curl command from step 3 and confirm each header appears exactly once.
  • Check your site on securityheaders.com.
  • Paste your policy into Google's CSP Evaluator to spot weak directives.

Troubleshooting

The CSP header is sent but empty

Another system plugin can interfere with the headers. Disable recently added system plugins one at a time until the policy appears.

You are locked out of the admin

Back up first: take a database backup before running any SQL. Replace #__ with your real table prefix.

Preview the plugin row, then disable it:

SELECT extension_id, name, enabled FROM `#__extensions`
WHERE `type` = 'plugin' AND `folder` = 'system' AND `element` = 'httpheaders';

UPDATE `#__extensions` SET `enabled` = 0
WHERE `type` = 'plugin' AND `folder` = 'system' AND `element` = 'httpheaders';

Log in again, then fix the Administrator rows before re-enabling the plugin.

Wrap-up

Baseline headers take five minutes. A good CSP takes patience: Report-Only first, allow only what you use, add a nonce, then enforce, one client at a time.

Share this article