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.
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
- Go to System → Manage → Plugins.
- Search for HTTP Headers and open System - HTTP Headers.
- 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).
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
curlcommand 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
#__ 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.
