{"id":392110,"date":"2026-11-06T14:45:02","date_gmt":"2026-11-06T14:45:02","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/codo-content-security-policy\/"},"modified":"2026-11-07T15:57:45","modified_gmt":"2026-11-07T15:57:45","slug":"codo-content-security-policy","status":"publish","type":"plugin","link":"https:\/\/wordpress.org\/plugins\/codo-content-security-policy\/","author":18270956,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"1.1.0","stable_tag":"1.1.0","tested":"7.1.4","requires":"6.2","requires_php":"7.4","requires_plugins":null,"header_name":"Codo Content Security Policy","header_author":"Codo Digital","header_description":"Content-Security-Policy headers for the front end and login page. Starts in Report-Only, with a draft policy for safe changes, built-in violation reports and asset scanning. Free, no upsells.","assets_banners_color":"3b6c7f","last_updated":"2026-11-07 15:57:45","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/github.com\/Codo-Digital-Ltd\/codo-content-security-policy","header_author_uri":"https:\/\/codo.digital","rating":0,"author_block_rating":0,"active_installs":0,"downloads":147,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.0.0":{"tag":"1.0.0","author":"cododigital","date":"2026-11-06 14:44:43","revision":3788127},"1.1.0":{"tag":"1.1.0","author":"cododigital","date":"2026-11-07 15:57:45","revision":3789853}},"upgrade_notice":{"1.1.0":"<p>Import rules found by a scan, with a safety review. New rules can no longer allow whole shared domains or public CDNs for scripts; existing rules keep working and are flagged.<\/p>","1.0.0":"<p>First release.<\/p>"},"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3788127,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3788127,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256},"icon.svg":{"filename":"icon.svg","revision":3788127,"resolution":false,"location":"assets","locale":false}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3788127,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3788127,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["1.0.0","1.1.0"],"block_files":[],"assets_screenshots":[],"screenshots":[]},"plugin_section":[],"plugin_tags":[7642,68184,19966,153786,14958],"plugin_category":[],"plugin_contributors":[284951],"plugin_business_model":[],"class_list":["post-392110","plugin","type-plugin","status-publish","hentry","plugin_tags-clickjacking","plugin_tags-content-security-policy","plugin_tags-csp","plugin_tags-security-headers","plugin_tags-xss","plugin_contributors-cododigital","plugin_committers-cododigital"],"banners":{"banner":"https:\/\/ps.w.org\/codo-content-security-policy\/assets\/banner-772x250.png?rev=3788127","banner_2x":"https:\/\/ps.w.org\/codo-content-security-policy\/assets\/banner-1544x500.png?rev=3788127","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":"https:\/\/ps.w.org\/codo-content-security-policy\/assets\/icon.svg?rev=3788127","icon":"https:\/\/ps.w.org\/codo-content-security-policy\/assets\/icon.svg?rev=3788127","icon_2x":false,"generated":false},"screenshots":[],"raw_content":"<!--section=description-->\n<p>A Content Security Policy tells browsers which scripts, styles, fonts, images, frames and form targets your pages may use. Anything else is blocked, which stops a whole class of attacks: scripts injected from other hosts, data sent to attackers' servers, forms hijacked to post elsewhere, and your site being framed for clickjacking.<\/p>\n\n<p>The hard part is building a policy without breaking the site. Codo Content Security Policy is designed around that.<\/p>\n\n<p>It is built by <a href=\"https:\/\/codo.digital\">Codo Digital<\/a>, a UK web development and managed hosting company, and is the policy plugin we use on our own clients' sites. Everything is free. There is no Pro version, no upsell, no account to create and no tracking.<\/p>\n\n<h4>Safe by default<\/h4>\n\n<ul>\n<li><strong>Starts in Report-Only.<\/strong> On activation the policy is sent as <code>Content-Security-Policy-Report-Only<\/code>: nothing is blocked, and browsers report what <em>would<\/em> be. When the reports are clean, enforce it with one button.<\/li>\n<li><strong>A sensible starting policy<\/strong> for WordPress: same-site resources, core's emoji images and emoji-test worker, Gravatar, no plugins (<code>object-src 'none'<\/code>), no <code>&lt;base&gt;<\/code> hijacking, forms posting only to your site, and no framing by other sites.<\/li>\n<li><strong>The admin area is never covered.<\/strong> Even a broken enforced policy cannot lock you out of the screen that fixes it. There is also a <code>wp-config.php<\/code> kill switch.<\/li>\n<\/ul>\n\n<h4>Changing an enforced policy without breaking it<\/h4>\n\n<ul>\n<li><strong>Adding a source applies straight away.<\/strong> Adding to an allowlist can only allow more, so it cannot break anything.<\/li>\n<li><strong>Tightening goes into a draft.<\/strong> Removing a source, adding a hash, or anything else that could block something loading today is held in a draft. The draft is sent as a Report-Only policy <em>alongside<\/em> the enforced one, so real visitors' browsers test it without anything being blocked. When its reports are clean, promote it.<\/li>\n<li><strong>New directives inherit.<\/strong> Creating <code>frame-src<\/code> for YouTube would normally stop it falling back to <code>default-src<\/code>, silently blocking your own iframes. The plugin copies the inherited sources across first.<\/li>\n<\/ul>\n\n<h4>Violation reports, built for slow sites<\/h4>\n\n<ul>\n<li>Reports are answered before your theme loads, with an empty <code>204<\/code> response.<\/li>\n<li>Chromium browsers batch reports, so one page view is usually one request.<\/li>\n<li>Identical reports are counted, not stored again, even when the same thing is blocked on every page. At most 500 different violations are kept, for 30 days. If the table fills up, the Reports tab says so rather than looking clean.<\/li>\n<li>Processing is rate-limited, and collection runs for a set period (1, 7 or 30 days) and then switches itself off.<\/li>\n<li>Each report has an <strong>Allow<\/strong> button. Anyone can send a report, so nothing is ever allowed automatically.<\/li>\n<li>You can send reports to an external collector (Sentry, report-uri.com and so on) instead.<\/li>\n<\/ul>\n\n<h4>Scanning<\/h4>\n\n<ul>\n<li>After you publish or update content, the page is fetched in the background as a logged-out visitor. Every script, stylesheet, font, image, iframe, media file and form target is checked against the policy, including the fonts and images inside your site's own stylesheets. Stylesheets hosted elsewhere are never downloaded; for Google Fonts and Bunny Fonts the font host is filled in automatically.<\/li>\n<li>Problems show in the posts list, in the editor, and on the Scan tab, each with an <strong>Allow<\/strong> button.<\/li>\n<li>You can scan any page, the login page, or your 20 most recently updated pages.<\/li>\n<li>Scans also warn when the server, a CDN or another plugin sends a second policy, and when the headers grow past 4 KB, which breaks NGINX's default FastCGI buffers.<\/li>\n<li>When a scan finds rules from another policy, you can import them alongside your own. Each rule is reviewed first: safe ones are ticked, risky ones need your decision, and unsafe ones are refused. Nothing is removed or duplicated.<\/li>\n<\/ul>\n\n<h4>Also<\/h4>\n\n<ul>\n<li><strong>Login page policy:<\/strong> wp-login.php gets its own rules, because reCAPTCHA, Turnstile and two-factor plugins load scripts there.<\/li>\n<li><strong>Presets<\/strong> for common services: Google Analytics, Tag Manager, Fonts and Maps, reCAPTCHA, Turnstile, YouTube, Vimeo, Stripe, PayPal, Gravatar, Jetpack, Meta Pixel, Hotjar and Microsoft Clarity.<\/li>\n<li><strong>Page builders keep working:<\/strong> no policy is sent in the Customizer preview or in Elementor, Divi, Beaver Builder, Bricks, Oxygen, WPBakery, Thrive, Brizy or Avada's front-end editors, for users who can edit.<\/li>\n<li><strong>Page caches are purged<\/strong> when the policy changes, so visitors get the new headers. Supported: WP Super Cache, W3 Total Cache, WP Rocket, LiteSpeed Cache, Nginx Helper, SiteGround Optimizer, WP-Optimize and Breeze.<\/li>\n<li><strong>Multisite:<\/strong> network defaults that every site adds to.<\/li>\n<li><strong>Post embeds keep working:<\/strong> <code>frame-ancestors<\/code> is left out of <code>\/embed\/<\/code> pages, which exist to be framed by other sites.<\/li>\n<li><strong>No JavaScript on the settings screen<\/strong>, so it cannot conflict with other plugins' admin scripts.<\/li>\n<\/ul>\n\n<h4>Why 'unsafe-inline' is in the starting policy<\/h4>\n\n<p>Almost every WordPress site prints inline scripts (core, themes and plugins all do). The strict alternatives don't fit most sites: nonces don't survive full-page caching, and hashes break whenever an inline script changes. So the starting policy keeps <code>'unsafe-inline'<\/code> for scripts and styles and works as an allowlist. It still blocks scripts from hosts you haven't listed, <code>eval<\/code>, plugins, <code>&lt;base&gt;<\/code> hijacking, form hijacking, data exfiltration through <code>connect-src<\/code>, and clickjacking.<\/p>\n\n<p>If your site can do without inline scripts, remove <code>'unsafe-inline'<\/code> from <code>script-src<\/code>. The change goes through a draft first, and the Reports tab shows exactly what breaks.<\/p>\n\n<h4>For hosts and agencies: configuration in wp-config.php<\/h4>\n\n<pre><code>define( 'CODO_CSP_DISABLE', true );                    \/\/ Send no policy headers at all.\ndefine( 'CODO_CSP_MODE', 'enforce' );                  \/\/ Or 'report-only'.\ndefine( 'CODO_CSP_REPORT_URI', 'https:\/\/example.report-uri.com\/r\/d\/csp\/enforce' );\ndefine( 'CODO_CSP_LOGIN', false );                     \/\/ No policy on wp-login.php.\n<\/code><\/pre>\n\n<p>Constants override the settings screen and show as read-only there.<\/p>\n\n<h4>For developers<\/h4>\n\n<ul>\n<li><code>codo_csp_headers<\/code> filter: change the headers before they are sent. Receives <code>array( name =&gt; array( values ) )<\/code> and the context (<code>site<\/code> or <code>login<\/code>).<\/li>\n<li><code>codo_csp_skip_request<\/code> filter: return true to send no policy on a front-end request.<\/li>\n<li><code>codo_csp_presets<\/code> filter: add or change presets.<\/li>\n<li><code>codo_csp_default_rules<\/code> filter: change the policy a site starts with.<\/li>\n<li><code>codo_csp_capability<\/code> filter: change who can manage the policy (default <code>manage_options<\/code>).<\/li>\n<li><code>codo_csp_report_rate_limit<\/code> filter: report requests processed per minute (default 120).<\/li>\n<li><code>codo_csp_max_report_rows<\/code> filter: different violations kept (default 500).<\/li>\n<li><code>codo_csp_policy_changed<\/code> action: fires when a policy changes. Hook in to purge a cache or CDN the plugin doesn't know about.<\/li>\n<li><code>codo_csp_purge_caches<\/code> action: fires after the plugin purges page caches.<\/li>\n<li><code>codo_csp_mode_changed<\/code> action: fires when switching between Report-Only and enforced.<\/li>\n<\/ul>\n\n<p>Source, issues and tests: <a href=\"https:\/\/github.com\/Codo-Digital-Ltd\/codo-content-security-policy\">github.com\/Codo-Digital-Ltd\/codo-content-security-policy<\/a>.<\/p>\n\n<h3>External services<\/h3>\n\n<p>The plugin does not contact any third-party service, collect usage data or track visitors or administrators.<\/p>\n\n<ul>\n<li><strong>Scanning stays on your site:<\/strong> scans request pages and stylesheets from your own site only (a loopback request, like Site Health makes), and refuse redirects to other sites. Stylesheets hosted elsewhere are never downloaded.<\/li>\n<li><strong>Presets are host names, not downloads:<\/strong> presets such as Stripe, Google Fonts or reCAPTCHA only add those services' host names to the policy your site sends, so your visitors' browsers are allowed to load what your theme and plugins already use. This plugin never loads anything from them.<\/li>\n<li><strong>External report collector (optional, off by default):<\/strong> if you enter a collector URL (or define <code>CODO_CSP_REPORT_URI<\/code>), your visitors' browsers send violation reports straight to that URL instead of your own site. Each report holds the page URL, the blocked URL, the directive and, depending on the browser, a short sample of the blocked code. Nothing is sent anywhere else if the setting is left empty. The terms and privacy policy are those of whoever runs the URL you choose.<\/li>\n<\/ul>\n\n<!--section=installation-->\n<ol>\n<li>Install from Plugins &gt; Add New, or upload the <code>codo-content-security-policy<\/code> folder to <code>\/wp-content\/plugins\/<\/code>.<\/li>\n<li>Activate the plugin. The policy starts in Report-Only and collects reports for 7 days.<\/li>\n<li>Go to Settings &gt; Content Security Policy. Add presets for the services your site uses, and use the Scan tab to check your main pages.<\/li>\n<li>Browse the site, then check the Reports tab. Allow what you recognise.<\/li>\n<li>When reports stop appearing, click <strong>Enforce the policy<\/strong>.<\/li>\n<\/ol>\n\n<!--section=faq-->\n<dl>\n<dt id=\"how%20is%20this%20different%20from%20security%20plugins%20that%20include%20csp%3F\"><h3>How is this different from security plugins that include CSP?<\/h3><\/dt>\n<dd><p>It only does Content Security Policy. Security suites tend to offer CSP as one setting among many, often a text box for the header with little help building it. This plugin is built around getting the policy right: it starts in Report-Only, collects violation reports on your own site, scans your pages for the sources they use, and puts changes to an enforced policy through a draft first.<\/p>\n\n<p>It adds no firewall, login protection or malware scanning, so it sits alongside whichever security plugin you already use. Just make sure that plugin isn't also sending a policy header; the scan warns you if one is.<\/p><\/dd>\n<dt id=\"will%20this%20break%20my%20site%3F\"><h3>Will this break my site?<\/h3><\/dt>\n<dd><p>Not while it's in Report-Only, which is how it starts: browsers only report. Enforce it once the Reports tab is quiet. After that, changes that could block something go through a draft first.<\/p><\/dd>\n<dt id=\"why%20aren%27t%20logged-in%20users%20excluded%3F\"><h3>Why aren't logged-in users excluded?<\/h3><\/dt>\n<dd><p>They're the users an attack on the front end usually targets, because their browser holds an admin session. So the front end gets the same policy for everyone. The only exceptions are page builders and the Customizer preview (see below). Note that the admin bar adds a few requests for logged-in users that visitors don't make, so expect a few reports that only logged-in users trigger.<\/p><\/dd>\n<dt id=\"my%20page%20builder%20or%20the%20customizer%20stopped%20working\"><h3>My page builder or the Customizer stopped working<\/h3><\/dt>\n<dd><p>Front-end editors inject inline code that an enforced policy blocks, so no policy is sent inside them by default. This applies to the Customizer preview and to Elementor, Divi, Beaver Builder, Bricks, Oxygen, WPBakery, Thrive, Brizy and Avada, for users who can edit. Make sure \"Page builders\" is ticked on the Settings tab. For other builders, use the <code>codo_csp_skip_request<\/code> filter.<\/p><\/dd>\n<dt id=\"i%20changed%20the%20policy%20but%20visitors%20still%20get%20the%20old%20one\"><h3>I changed the policy but visitors still get the old one<\/h3><\/dt>\n<dd><p>Full-page caches store the headers with the page. The plugin purges the common cache plugins when the policy changes. For anything else, such as Cloudflare, Varnish, NGINX <code>fastcgi_cache<\/code> without Nginx Helper, or your host's cache, purge it yourself or hook <code>codo_csp_purge_caches<\/code>.<\/p><\/dd>\n<dt id=\"why%20does%20the%20scan%20say%20another%20policy%20header%20is%20being%20sent%3F\"><h3>Why does the scan say another policy header is being sent?<\/h3><\/dt>\n<dd><p>Browsers enforce every policy they receive, so a second one (from your server configuration, a CDN, a security plugin or your host) can block something this plugin allows. Remove the other one, or make sure it allows at least as much. A page cache still holding an older version of this plugin's header can also cause the warning; purge it and scan again.<\/p><\/dd>\n<dt id=\"my%20site%20returns%20502%20errors%20after%20adding%20many%20sources\"><h3>My site returns 502 errors after adding many sources<\/h3><\/dt>\n<dd><p>NGINX's default FastCGI buffer is 4 KB or 8 KB, and a large policy can exceed it (\"upstream sent too big header\"). The policy screen shows the header size and warns over 4 KB. Remove sources you no longer need (wildcards like <code>https:\/\/*.example.com<\/code> can replace several hosts), or raise <code>fastcgi_buffer_size<\/code> and <code>fastcgi_buffers<\/code>.<\/p><\/dd>\n<dt id=\"why%20can%27t%20i%20add%20%27https%3A%27%20or%20%2A%20to%20script-src%3F\"><h3>Why can't I add 'https:' or * to script-src?<\/h3><\/dt>\n<dd><p>It would allow scripts from any host, which defeats the policy. Add the specific hosts instead. The same applies to <code>https:\/\/*<\/code>, <code>data:<\/code> and <code>blob:<\/code>, and to <code>default-src<\/code> (which covers scripts when <code>script-src<\/code> is absent), <code>object-src<\/code>, <code>worker-src<\/code> and <code>base-uri<\/code>. Workers may use <code>blob:<\/code>, because WordPress's emoji loader needs it.<\/p><\/dd>\n<dt id=\"why%20can%27t%20i%20add%20a%20nonce%3F\"><h3>Why can't I add a nonce?<\/h3><\/dt>\n<dd><p>Nonces must change on every response, so they can't be a fixed source. They also don't survive full-page caching. Use a hash for a specific inline script, keeping in mind that browsers ignore <code>'unsafe-inline'<\/code> once a hash is present.<\/p><\/dd>\n<dt id=\"a%20report%20says%20something%20was%20blocked%20on%20a%20page%20i%20don%27t%20recognise\"><h3>A report says something was blocked on a page I don't recognise<\/h3><\/dt>\n<dd><p>Anyone can send reports to your site, and browser extensions inject content that violates every policy (the plugin drops reports it can identify as extensions). Only allow sources you recognise. Sources a scan also found are marked.<\/p><\/dd>\n<dt id=\"do%20reports%20slow%20the%20site%20down%3F\"><h3>Do reports slow the site down?<\/h3><\/dt>\n<dd><p>Very little. They are handled before the theme loads and <code>init<\/code> runs, and processing is rate-limited (120 requests a minute by default). Identical reports update a counter instead of adding rows. Collection also switches itself off at the end of the period you choose.<\/p><\/dd>\n<dt id=\"how%20long%20are%20reports%20kept%3F\"><h3>How long are reports kept?<\/h3><\/dt>\n<dd><p>Reports not seen for 30 days are deleted by a daily WP-Cron task, and you can clear them at any time. Reports store the directive, the blocked origin (not the full URL) and the latest page it happened on (the path, not the query string). No IP addresses or user agents are stored.<\/p><\/dd>\n<dt id=\"does%20the%20plugin%20send%20data%20to%20codo%20digital%3F\"><h3>Does the plugin send data to Codo Digital?<\/h3><\/dt>\n<dd><p>No.<\/p><\/dd>\n<dt id=\"is%20it%20multisite%20compatible%3F\"><h3>Is it multisite compatible?<\/h3><\/dt>\n<dd><p>Yes. Each site has its own policy, mode and reports, managed by its administrators. Network administrators can add network defaults (Network Admin &gt; Settings &gt; Content Security Policy). These are merged into every site's policy and can't be removed by site administrators. A network default that creates a directive on a site keeps the sources that site inherited for it, so adding one never blocks a site's own resources. <code>'none'<\/code>, hashes and <code>'strict-dynamic'<\/code> can't be network defaults. Removing a network default applies to every site straight away, without a draft.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>1.1.0<\/h4>\n\n<ul>\n<li>New: import rules from another policy found by a scan, with a safety review of every rule. Existing rules are kept and never duplicated.<\/li>\n<li>Changed: new rules can no longer allow a whole shared domain (such as *.cloudfront.net), or a whole public CDN (such as cdn.jsdelivr.net) for scripts. Rules you already have keep working and are flagged on the policy screen.<\/li>\n<li>Changed: when a whole host would be refused, a scan's Allow button offers the exact file instead.<\/li>\n<\/ul>\n\n<h4>1.0.0<\/h4>\n\n<ul>\n<li>First release: front-end and login policies, Report-Only by default, enforced mode with draft policies, built-in aggregated violation reports, page scanning after publishing, presets, page builder exclusions, cache purging, multisite network defaults and wp-config.php constants.<\/li>\n<\/ul>","raw_excerpt":"Just Content-Security-Policy, not a security suite. Starts in Report-Only, with draft policies, built-in violation reports and page scanning.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/392110","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=392110"}],"author":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/cododigital"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=392110"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=392110"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=392110"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=392110"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=392110"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=392110"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}