A well-configured Content Security Policy blocks the most common XSS vectors before they reach your users. JekCMS ships with a permissive default CSP that works with all themes; tightening it requires knowing which external resources each component actually loads.
The default JekCMS CSP includes 'unsafe-inline' for both scripts and styles. This is intentional: the admin panel uses inline event handlers in several components, and the default themes inline critical CSS for performance reasons. Before tightening the policy, audit both the admin panel and your active theme by running the site with a strict CSP in report-only mode and reviewing the violation reports.
Self-Hosting Fonts Removes Two Directives
Fonts loaded from Google Fonts require two directives: font-src fonts.gstatic.com and style-src fonts.googleapis.com. If you switch to self-hosted fonts — which we recommend for both privacy and performance — you can replace both with 'self' and remove the external font dependencies entirely. JekCMS's default Trends theme ships with self-hosted Inter subsets that cover Latin and Turkish character ranges.
Explicit img-src Enumeration
The img-src directive needs to include every external domain from which your content loads images. A common mistake is setting img-src https: to allow any HTTPS image source — this defeats most of the CSP's value for image-based attacks. Enumerate your actual image sources explicitly: img-src 'self' cdn.yoursite.com data:.
Always Start in Report-Only Mode
Start with Report-Only mode before enforcing: set Content-Security-Policy-Report-Only with a report-uri pointing to a collection endpoint. The free tier of report-uri.com works well for small sites. Collect violations for at least a week before switching to enforcing mode — weekend traffic often exercises code paths that weekday monitoring misses.
The Complete JekCMS CSP Template
Here is the production-ready CSP header that works with most JekCMS themes and common third-party services:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline' https://www.googletagmanager.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://www.google-analytics.com;
font-src 'self';
connect-src 'self' https://www.google-analytics.com;
frame-src 'self' https://www.youtube.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'self';
upgrade-insecure-requests;
Directive-by-Directive Explanation
default-src 'self'— baseline for any resource type not explicitly listed; restricts everything to the same originobject-src 'none'— blocks Flash, Java applets, and other plugin content; common XSS vectors with no modern usebase-uri 'self'— prevents attackers from injecting a base tag to redirect relative URLs to a malicious domainframe-ancestors 'self'— equivalent to X-Frame-Options: SAMEORIGIN but with broader browser support; prevents clickjackingupgrade-insecure-requests— automatically converts HTTP to HTTPS; catches mixed content from older database entries
Google AdSense Additions
Sites running Google AdSense need additional directives because Google's ad network uses a rotating set of domains. The minimum additions:
script-src https://pagead2.googlesyndication.com https://adservice.google.com;
img-src https://pagead2.googlesyndication.com;
frame-src https://googleads.g.doubleclick.net https://tpc.googlesyndication.com;
AdSense violations are the number one reason sites abandon strict CSP policies. We observed 3 new domain additions over a 6-month period on our monitored sites. If you run AdSense, keep Report-Only mode permanently alongside your enforcing header.
Common Mistakes and Debugging
- Using
unsafe-evalbecause a library uses eval() — find an alternative library instead - Whitelisting CDN domains broadly (e.g.,
*.cloudflare.com) — attackers can host scripts on the same CDN - Forgetting
connect-srcfor AJAX requests — breaks admin panel functionality silently - Not testing the admin panel separately — it loads different scripts than the frontend
Nonce-Based Script Loading
For sites that want to eliminate 'unsafe-inline' from the script directive, JekCMS supports nonce-based script loading. The Security class generates a random nonce on each request and makes it available via csp_nonce(). Every inline script must include this nonce attribute. The migration from unsafe-inline to nonces typically requires updating 15-25 inline script tags across admin and theme files.
// Security.php generates the nonce
$nonce = bin2hex(random_bytes(16));
// In header.php - use the nonce
<script nonce="<?= csp_nonce() ?>">
// This inline script is allowed by CSP
document.documentElement.className += ' js-enabled';
</script>
// CSP header includes the nonce
script-src 'nonce-{generated_nonce}' 'strict-dynamic';
Monitoring CSP in Production
After deploying a CSP, set up ongoing monitoring. JekCMS logs CSP violations to the audit_log table when a report-uri points to your own site. Review these logs weekly for the first month after deployment.
The three most common violation sources we have observed across 47 sites are: browser extensions injecting scripts (60% of all violations), outdated content referencing HTTP resources (25%), and third-party analytics services changing their domain infrastructure (15%). Only the last category requires CSP policy updates.
CSP for the Admin Panel vs. Frontend
A common mistake is applying the same CSP to both the admin panel and the public frontend. The admin panel requires more permissive rules because it uses inline event handlers for drag-and-drop media uploads, CodeMirror syntax highlighting, and WYSIWYG editors.
JekCMS applies a separate, relaxed policy to all /admin/ routes while keeping the frontend strict. The admin policy adds 'unsafe-eval' for CodeMirror and blob: for image preview thumbnails. This dual-policy approach means the public-facing pages maintain an A+ security rating on Observatory even when the admin panel uses features that would otherwise weaken the score.
To implement this, the Security.php middleware checks the request URI before setting CSP headers. If the path starts with /admin/ and the user is authenticated, the admin policy is applied. All other requests receive the strict frontend policy. This check runs before any output is sent, ensuring headers are set correctly regardless of template caching.