{"id":1998,"date":"2025-11-17T22:41:12","date_gmt":"2025-11-17T19:41:12","guid":{"rendered":"https:\/\/www.dchost.com\/blog\/csp-done-right-how-i-use-nonces-hashes-and-report-to-to-tame-inline-scripts-on-wordpress-and-laravel\/"},"modified":"2025-11-17T22:41:12","modified_gmt":"2025-11-17T19:41:12","slug":"csp-done-right-how-i-use-nonces-hashes-and-report-to-to-tame-inline-scripts-on-wordpress-and-laravel","status":"publish","type":"post","link":"https:\/\/www.dchost.com\/blog\/en\/csp-done-right-how-i-use-nonces-hashes-and-report-to-to-tame-inline-scripts-on-wordpress-and-laravel\/","title":{"rendered":"CSP Done Right: How I Use Nonces, Hashes, and report-to to Tame Inline Scripts on WordPress and Laravel"},"content":{"rendered":"<div class=\"dchost-blog-content-wrapper\"><p>So there I was, staring at a client&#8217;s WordPress dashboard at 1 a.m., wondering why the homepage was randomly breaking only for some visitors. The site still loaded, but half the buttons didn\u2019t respond. Classic ghost bug. After a quick sweep, it turned out a harmless-looking plugin slipped in a new inline script. On its own, no big deal. But combined with a strict Content Security Policy (CSP), it was blocked right out of the gate. That night was my reminder: CSP is powerful, but it\u2019s also unforgiving if you don\u2019t set it up with a plan\u2014especially on platforms that love inline scripts like WordPress and, in a different way, Laravel.<\/p>\n<p>If you\u2019ve ever had a page work in staging and then die in production, or you\u2019ve tried to stamp out XSS while keeping marketing scripts happy, you\u2019ll feel right at home here. In this guide, I\u2019ll walk you through how I set up CSP with <strong>nonces<\/strong> and <strong>hashes<\/strong>, make sense of <strong>report-to<\/strong> (plus a report-uri fallback), and gently tame those inline scripts in WordPress and Laravel without losing your mind\u2014or your analytics. We\u2019ll talk real-world gotchas, practical config snippets, and a rollout plan you can actually ship.<\/p>\n<div id=\"toc_container\" class=\"toc_transparent no_bullets\"><p class=\"toc_title\">\u0130&ccedil;indekiler<\/p><ul class=\"toc_list\"><li><a href=\"#The_Real_Reason_CSP_Matters_and_Why_It_Bites_First_in_WordPress\"><span class=\"toc_number toc_depth_1\">1<\/span> The Real Reason CSP Matters (and Why It Bites First in WordPress)<\/a><\/li><li><a href=\"#CSP_in_Plain_English_The_Bouncers_Rules\"><span class=\"toc_number toc_depth_1\">2<\/span> CSP in Plain English: The Bouncer\u2019s Rules<\/a><\/li><li><a href=\"#Nonces_vs_Hashes_When_to_Use_What_and_Why_strict-dynamic_Helps\"><span class=\"toc_number toc_depth_1\">3<\/span> Nonces vs Hashes: When to Use What (and Why strict-dynamic Helps)<\/a><ul><li><a href=\"#Nonces_for_dynamic_hashes_for_static\"><span class=\"toc_number toc_depth_2\">3.1<\/span> Nonces for dynamic, hashes for static<\/a><\/li><li><a href=\"#Why_I_often_add_strict-dynamic\"><span class=\"toc_number toc_depth_2\">3.2<\/span> Why I often add strict-dynamic<\/a><\/li><li><a href=\"#A_baseline_policy_I_like\"><span class=\"toc_number toc_depth_2\">3.3<\/span> A baseline policy I like<\/a><\/li><\/ul><\/li><li><a href=\"#WordPress_Taming_Inline_Scripts_Without_Breaking_the_Theme\"><span class=\"toc_number toc_depth_1\">4<\/span> WordPress: Taming Inline Scripts Without Breaking the Theme<\/a><ul><li><a href=\"#The_game_plan\"><span class=\"toc_number toc_depth_2\">4.1<\/span> The game plan<\/a><\/li><li><a href=\"#Generate_a_nonce_per_request_and_send_CSP_headers\"><span class=\"toc_number toc_depth_2\">4.2<\/span> Generate a nonce per request and send CSP headers<\/a><\/li><li><a href=\"#Moving_away_from_inline_handlers\"><span class=\"toc_number toc_depth_2\">4.3<\/span> Moving away from inline handlers<\/a><\/li><li><a href=\"#About_third-party_tags\"><span class=\"toc_number toc_depth_2\">4.4<\/span> About third-party tags<\/a><\/li><\/ul><\/li><li><a href=\"#Laravel_Middleware_Blade_and_a_Clean_Nonce_Flow\"><span class=\"toc_number toc_depth_1\">5<\/span> Laravel: Middleware, Blade, and a Clean Nonce Flow<\/a><ul><li><a href=\"#Middleware_that_just_works\"><span class=\"toc_number toc_depth_2\">5.1<\/span> Middleware that just works<\/a><\/li><li><a href=\"#Using_the_nonce_in_Blade\"><span class=\"toc_number toc_depth_2\">5.2<\/span> Using the nonce in Blade<\/a><\/li><li><a href=\"#Dealing_with_vendor_scripts\"><span class=\"toc_number toc_depth_2\">5.3<\/span> Dealing with vendor scripts<\/a><\/li><\/ul><\/li><li><a href=\"#Collecting_CSP_Reports_report-to_report-uri_and_the_Gentle_Rollout\"><span class=\"toc_number toc_depth_1\">6<\/span> Collecting CSP Reports: report-to, report-uri, and the Gentle Rollout<\/a><ul><li><a href=\"#Why_reports_matter\"><span class=\"toc_number toc_depth_2\">6.1<\/span> Why reports matter<\/a><\/li><li><a href=\"#Use_Report-Only_first_then_enforce\"><span class=\"toc_number toc_depth_2\">6.2<\/span> Use Report-Only first, then enforce<\/a><\/li><li><a href=\"#report-to_and_report-uri_together\"><span class=\"toc_number toc_depth_2\">6.3<\/span> report-to and report-uri, together<\/a><\/li><li><a href=\"#Analyze_adjust_and_roll_out_gradually\"><span class=\"toc_number toc_depth_2\">6.4<\/span> Analyze, adjust, and roll out gradually<\/a><\/li><\/ul><\/li><li><a href=\"#From_Inline_Chaos_to_Calm_A_Practical_Migration_Path\"><span class=\"toc_number toc_depth_1\">7<\/span> From Inline Chaos to Calm: A Practical Migration Path<\/a><ul><li><a href=\"#1_Start_with_a_clear_minimal_policy_in_Report-Only\"><span class=\"toc_number toc_depth_2\">7.1<\/span> 1) Start with a clear, minimal policy in Report-Only<\/a><\/li><li><a href=\"#2_Enqueue_and_externalize\"><span class=\"toc_number toc_depth_2\">7.2<\/span> 2) Enqueue and externalize<\/a><\/li><li><a href=\"#3_Replace_inline_handlers_with_event_listeners\"><span class=\"toc_number toc_depth_2\">7.3<\/span> 3) Replace inline handlers with event listeners<\/a><\/li><li><a href=\"#4_Hash_the_rare_stable_inline_block\"><span class=\"toc_number toc_depth_2\">7.4<\/span> 4) Hash the rare, stable inline block<\/a><\/li><li><a href=\"#5_Document_your_choices\"><span class=\"toc_number toc_depth_2\">7.5<\/span> 5) Document your choices<\/a><\/li><\/ul><\/li><li><a href=\"#NginxApacheCDN_Where_to_Set_the_Headers\"><span class=\"toc_number toc_depth_1\">8<\/span> Nginx\/Apache\/CDN: Where to Set the Headers<\/a><ul><li><a href=\"#Nginx_example_nonce_placeholder\"><span class=\"toc_number toc_depth_2\">8.1<\/span> Nginx example (nonce placeholder)<\/a><\/li><\/ul><\/li><li><a href=\"#Common_Gotchas_and_the_Friendly_Fixes\"><span class=\"toc_number toc_depth_1\">9<\/span> Common Gotchas and the Friendly Fixes<\/a><ul><li><a href=\"#It_works_locally_but_not_in_production\"><span class=\"toc_number toc_depth_2\">9.1<\/span> \u201cIt works locally but not in production\u201d<\/a><\/li><li><a href=\"#My_inline_JSON_broke\"><span class=\"toc_number toc_depth_2\">9.2<\/span> \u201cMy inline JSON broke\u201d<\/a><\/li><li><a href=\"#I_need_eval_for_a_vendor\"><span class=\"toc_number toc_depth_2\">9.3<\/span> \u201cI need eval for a vendor\u201d<\/a><\/li><li><a href=\"#Blocking_looks_random\"><span class=\"toc_number toc_depth_2\">9.4<\/span> \u201cBlocking looks random\u201d<\/a><\/li><li><a href=\"#WordPress_core_or_a_plugin_still_injects_inline\"><span class=\"toc_number toc_depth_2\">9.5<\/span> \u201cWordPress core or a plugin still injects inline\u201d<\/a><\/li><\/ul><\/li><li><a href=\"#Security_Is_a_Team_Sport_Pair_CSP_with_Good_Hygiene\"><span class=\"toc_number toc_depth_1\">10<\/span> Security Is a Team Sport: Pair CSP with Good Hygiene<\/a><\/li><li><a href=\"#Wrap-Up_Your_Calm_CSP_Playbook\"><span class=\"toc_number toc_depth_1\">11<\/span> Wrap-Up: Your Calm CSP Playbook<\/a><\/li><li><a href=\"#Helpful_References_I_Like\"><span class=\"toc_number toc_depth_1\">12<\/span> Helpful References I Like<\/a><\/li><\/ul><\/div>\n<h2 id=\"section-1\"><span id=\"The_Real_Reason_CSP_Matters_and_Why_It_Bites_First_in_WordPress\">The Real Reason CSP Matters (and Why It Bites First in WordPress)<\/span><\/h2>\n<p>I like to think of CSP as the bouncer at the club. Without it, anyone who looks vaguely like JavaScript can stroll right in and start handing out dodgy links. With it, you have a guest list: only scripts from these places, in these formats, under these conditions. Clean and calm.<\/p>\n<p>But here\u2019s the thing\u2014WordPress and many themes\/plugins sprinkle inline scripts like confetti. A tiny initialization here, a <code>wp_localize_script<\/code> JSON blob there, maybe an inline jQuery snippet in the footer. Laravel\u2019s Blade templates aren\u2019t immune either; you might be echoing variables into scripts or dropping small inline helpers during prototyping. All of these run headfirst into CSP the moment you remove <code>'unsafe-inline'<\/code>, which you <strong>should<\/strong> remove if you want CSP to actually protect you from XSS.<\/p>\n<p>In my experience, the first time you enable a strict CSP, the console lights up like a Christmas tree. That\u2019s not failure\u2014that\u2019s visibility. CSP shows you where the inline landmines are so you can migrate them to safer patterns: <strong>nonces<\/strong> for per-request whitelisting, <strong>hashes<\/strong> for static inline blocks, and better yet, external scripts with no inline at all. Once you\u2019ve tamed the inline chaos, you get a policy that actually holds the line.<\/p>\n<h2 id=\"section-2\"><span id=\"CSP_in_Plain_English_The_Bouncers_Rules\">CSP in Plain English: The Bouncer\u2019s Rules<\/span><\/h2>\n<p>Let\u2019s keep it simple. A CSP header is just a list of directives. <code>default-src<\/code> is your catch-all, <code>script-src<\/code> is specifically for JavaScript, <code>style-src<\/code> for CSS, and so on. You can point to specific origins (like your domain and a CDN), and you can add special tokens that unlock safer inline behavior.<\/p>\n<p>The usual troublemaker is <code>'unsafe-inline'<\/code>. It\u2019s convenient\u2014everything inline runs\u2014but it more or less disables CSP\u2019s protection against inline XSS. If a malicious input ends up in your page, inline JS executes. With CSP done right, we ditch <code>'unsafe-inline'<\/code> and replace it with <strong>nonces<\/strong> or <strong>hashes<\/strong>. The policy can also say, \u201ctrust this nonce, and anything that script loads,\u201d using <code>'strict-dynamic'<\/code>, which is a powerful friend when you\u2019re moving away from older patterns.<\/p>\n<p>Think of it like this: nonces are like unique wristbands you hand out at the door for each request; hashes are like a signature you\u2019ve pre-approved because you know this exact block of code is safe. Both let you keep the bouncer tough without turning away your friends.<\/p>\n<h2 id=\"section-3\"><span id=\"Nonces_vs_Hashes_When_to_Use_What_and_Why_strict-dynamic_Helps\">Nonces vs Hashes: When to Use What (and Why strict-dynamic Helps)<\/span><\/h2>\n<h3><span id=\"Nonces_for_dynamic_hashes_for_static\">Nonces for dynamic, hashes for static<\/span><\/h3>\n<p>Here\u2019s my rule of thumb. If an inline script is <strong>generated per request<\/strong> or it might change a lot (think localized data, server-injected variables), use a <strong>nonce<\/strong>. You generate a random base64 nonce on the server for every request, add it to the CSP header (something like <code>script-src 'nonce-abc123'<\/code>), and then tag each inline script with <code>nonce=\"abc123\"<\/code>. The browser checks the nonce in the tag against the header and allows it if they match.<\/p>\n<p>If a block is <strong>static and tiny<\/strong> (for example, a well-known initialization snippet that hardly ever changes), you can use a <strong>hash<\/strong> instead. Hashes require calculating a SHA-256 (or 384\/512) of the <em>exact<\/em> inline content. If you change even one character, the hash breaks and the browser blocks it. Perfect for little constants; annoying for anything you edit often.<\/p>\n<h3><span id=\"Why_I_often_add_strict-dynamic\">Why I often add strict-dynamic<\/span><\/h3>\n<p><code>'strict-dynamic'<\/code> lets trusted scripts (ones allowed via a nonce or hash) load other scripts, and those child scripts inherit trust\u2014even if they come from a new origin that\u2019s not listed. This is lovely when you have a loader script that pulls the rest of your app. With <code>'strict-dynamic'<\/code>, you\u2019re telling the browser, \u201cif the first one is trusted, let it bootstrap the rest.\u201d<\/p>\n<h3><span id=\"A_baseline_policy_I_like\">A baseline policy I like<\/span><\/h3>\n<p>For many sites, this is a clean starting point when you\u2019re ready to enforce:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">Content-Security-Policy: \n  default-src 'self'; \n  script-src 'self' 'nonce-REPLACE_ME' 'strict-dynamic' https:; \n  style-src 'self' 'nonce-REPLACE_ME' https:; \n  img-src 'self' data: https:; \n  font-src 'self' https: data:; \n  connect-src 'self' https:; \n  frame-ancestors 'self'; \n  base-uri 'self'; \n  object-src 'none'; \n  report-to csp-endpoint; \n  report-uri https:\/\/report.example.com\/csp\n<\/code><\/pre>\n<p>You\u2019ll notice I used both <code>report-to<\/code> and <code>report-uri<\/code>. Browser support has shifted around over the years, so I still include both. That way, you get reports in more places while the ecosystem continues to settle.<\/p>\n<h2 id=\"section-4\"><span id=\"WordPress_Taming_Inline_Scripts_Without_Breaking_the_Theme\">WordPress: Taming Inline Scripts Without Breaking the Theme<\/span><\/h2>\n<h3><span id=\"The_game_plan\">The game plan<\/span><\/h3>\n<p>WordPress loves inline scripts. Plugins add <code>wp_localize_script<\/code> blobs, themes sprinkle helper inits, and someone somewhere always left an <code>onclick<\/code> in a template. The trick isn\u2019t to banish them in one day. The trick is steady, friendly discipline: add nonces everywhere you can, migrate inline handlers to <code>addEventListener<\/code>, and move long inline code into enqueued files.<\/p>\n<h3><span id=\"Generate_a_nonce_per_request_and_send_CSP_headers\">Generate a nonce per request and send CSP headers<\/span><\/h3>\n<p>I like dropping this into a small MU-plugin or a site plugin so it\u2019s always loaded:<\/p>\n<pre class=\"language-php line-numbers\"><code class=\"language-php\">&lt;?php\n\/\/ wp-content\/mu-plugins\/csp.php\nif (!defined('ABSPATH')) { exit; }\n\nadd_action('init', function () {\n    \/\/ Per-request nonce\n    $nonce = base64_encode(random_bytes(16));\n    $GLOBALS['csp_nonce'] = $nonce;\n});\n\nadd_action('send_headers', function () {\n    if (headers_sent()) { return; }\n    $nonce = isset($GLOBALS['csp_nonce']) ? $GLOBALS['csp_nonce'] : '';\n\n    \/\/ Define your reporting endpoints\n    \/\/ Keep report-uri as a fallback while report-to\/reporting endpoints evolve\n    $reportUri = 'https:\/\/report.example.com\/csp';\n\n    \/\/ Note: Some installs use a reverse proxy\/CDN for headers; you can move this there if you prefer.\n    $csp = &quot;default-src 'self'; &quot; .\n           &quot;script-src 'self' 'nonce-{$nonce}' 'strict-dynamic' https:; &quot; .\n           &quot;style-src 'self' 'nonce-{$nonce}' https:; &quot; .\n           &quot;img-src 'self' data: https:; &quot; .\n           &quot;font-src 'self' https: data:; &quot; .\n           &quot;connect-src 'self' https:; &quot; .\n           &quot;frame-ancestors 'self'; base-uri 'self'; object-src 'none'; &quot; .\n           &quot;report-to csp-endpoint; report-uri {$reportUri}&quot;;\n\n    header('Content-Security-Policy: ' . $csp);\n\n    \/\/ Optional: Report-Only during rollout\n    \/\/ header('Content-Security-Policy-Report-Only: ' . $csp);\n\n    \/\/ Define reporting endpoints for browsers that support this\n    header(&quot;Report-To: {&quot;group&quot;:&quot;csp-endpoint&quot;,&quot;max_age&quot;:10886400,&quot;endpoints&quot;:[{&quot;url&quot;:&quot;{$reportUri}&quot;}]}&quot;);\n    header(&quot;Reporting-Endpoints: csp-endpoint=&quot;{$reportUri}&quot;&quot;);\n});\n\n\/\/ Add nonce to enqueued scripts\nadd_filter('script_loader_tag', function ($tag) {\n    if (is_admin()) { return $tag; }\n    if (!empty($GLOBALS['csp_nonce'])) {\n        \/\/ Insert nonce attribute if not present\n        if (false === strpos($tag, 'nonce=')) {\n            $tag = preg_replace('\/&lt;script(s+)\/', '&lt;script nonce=&quot;' . esc_attr($GLOBALS['csp_nonce']) . '&quot; $1', $tag, 1);\n        }\n    }\n    return $tag;\n}, 10, 1);\n\n\/\/ Add nonce to enqueued styles\nadd_filter('style_loader_tag', function ($tag) {\n    if (is_admin()) { return $tag; }\n    if (!empty($GLOBALS['csp_nonce'])) {\n        if (false === strpos($tag, 'nonce=')) {\n            $tag = preg_replace('\/&lt;link(s+)\/', '&lt;link nonce=&quot;' . esc_attr($GLOBALS['csp_nonce']) . '&quot; $1', $tag, 1);\n        }\n    }\n    return $tag;\n}, 10, 1);\n\n\/\/ Add nonce to inline scripts\/styles if your WP version exposes these filters\n\/\/ These filters exist in modern WP versions and make life easier\nadd_filter('wp_print_inline_script_tag', function ($tag) {\n    if (!empty($GLOBALS['csp_nonce']) &amp;&amp; false === strpos($tag, 'nonce=')) {\n        $tag = str_replace('&lt;script', '&lt;script nonce=&quot;' . esc_attr($GLOBALS['csp_nonce']) . '&quot;', $tag);\n    }\n    return $tag;\n}, 10, 1);\n\nadd_filter('wp_print_inline_style_tag', function ($tag) {\n    if (!empty($GLOBALS['csp_nonce']) &amp;&amp; false === strpos($tag, 'nonce=')) {\n        $tag = str_replace('&lt;style', '&lt;style nonce=&quot;' . esc_attr($GLOBALS['csp_nonce']) . '&quot;', $tag);\n    }\n    return $tag;\n}, 10, 1);\n<\/code><\/pre>\n<p>This tiny helper covers a lot of ground. It generates a per-request nonce, sets a CSP header, and injects <code>nonce<\/code> into both external and inline tags\u2014so your <code>wp_localize_script<\/code> and other inline bits stay allowed without blowing the door open with <code>'unsafe-inline'<\/code>. If your theme or plugins output raw inline tags outside the enqueue system, you might have to patch those templates or nudge developers to use <code>wp_enqueue_script<\/code> properly.<\/p>\n<h3><span id=\"Moving_away_from_inline_handlers\">Moving away from inline handlers<\/span><\/h3>\n<p>Inline attributes like <code>onclick<\/code>, <code>onload<\/code>, and friends are kryptonite for CSP. The healthier pattern is a short external script that attaches listeners after <code>DOMContentLoaded<\/code>. For example:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">\/\/ main.js\nwindow.addEventListener('DOMContentLoaded', () =&gt; {\n  document.querySelectorAll('[data-action=&quot;do-thing&quot;]').forEach(btn =&gt; {\n    btn.addEventListener('click', (e) =&gt; {\n      \/\/ your logic here\n    });\n  });\n});\n<\/code><\/pre>\n<p>Then in your HTML, use <code>data-<\/code> attributes, not inline JS:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">&lt;button data-action=&quot;do-thing&quot;&gt;Click me&lt;\/button&gt;\n<\/code><\/pre>\n<p>That tiny change lets you remove inline handlers while keeping your CSP clean. Over time, those small refactors add up to a site that\u2019s both safer and easier to reason about.<\/p>\n<h3><span id=\"About_third-party_tags\">About third-party tags<\/span><\/h3>\n<p>Third-party scripts can be tricky with strict CSP\u2014especially tag managers or A\/B testing tools that love to inject inline code. If you absolutely must use them, prefer loader tags that work with external files and nonces. Keep a short allowlist in <code>script-src<\/code> for the domains you trust, and consider <strong>staging-only testing<\/strong> before you push any new vendor script to production. When I\u2019m shipping bigger changes, I often roll CSP out in Report-Only first, similar to how I describe canaries in <a href=\"https:\/\/www.dchost.com\/blog\/en\/vpste-canary-dagitimi-nasil-tatli-tatli-kurulur-nginx-agirlikli-yonlendirme-saglik-kontrolu-ve-guvenli-rollback\/\">a friendly guide to canary deploys with weighted routing and safe rollbacks<\/a>.<\/p>\n<h2 id=\"section-5\"><span id=\"Laravel_Middleware_Blade_and_a_Clean_Nonce_Flow\">Laravel: Middleware, Blade, and a Clean Nonce Flow<\/span><\/h2>\n<h3><span id=\"Middleware_that_just_works\">Middleware that just works<\/span><\/h3>\n<p>Laravel gives you a beautiful place to generate a nonce and share it with your views. I usually add a middleware like this:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">php artisan make:middleware AddCspHeaders\n<\/code><\/pre>\n<pre class=\"language-php line-numbers\"><code class=\"language-php\">&lt;?php\n\/\/ app\/Http\/Middleware\/AddCspHeaders.php\n\nnamespace AppHttpMiddleware;\n\nuse Closure;\nuse IlluminateHttpRequest;\nuse SymfonyComponentHttpFoundationResponse;\n\nclass AddCspHeaders\n{\n    public function handle(Request $request, Closure $next): Response\n    {\n        $response = $next($request);\n\n        \/\/ Generate per-request nonce\n        $nonce = base64_encode(random_bytes(16));\n        app()-&gt;instance('csp-nonce', $nonce);\n\n        \/\/ Build CSP\n        $reportUri = config('security.csp_report_uri', 'https:\/\/report.example.com\/csp');\n        $csp = &quot;default-src 'self'; &quot; .\n               &quot;script-src 'self' 'nonce-{$nonce}' 'strict-dynamic' https:; &quot; .\n               &quot;style-src 'self' 'nonce-{$nonce}' https:; &quot; .\n               &quot;img-src 'self' data: https:; font-src 'self' https: data:; &quot; .\n               &quot;connect-src 'self' https:; frame-ancestors 'self'; base-uri 'self'; object-src 'none'; &quot; .\n               &quot;report-to csp-endpoint; report-uri {$reportUri}&quot;;\n\n        \/\/ Set headers (swap to Report-Only during rollout if you prefer)\n        $response-&gt;headers-&gt;set('Content-Security-Policy', $csp, false);\n        $response-&gt;headers-&gt;set('Report-To', json_encode([\n            'group' =&gt; 'csp-endpoint',\n            'max_age' =&gt; 10886400,\n            'endpoints' =&gt; [['url' =&gt; $reportUri]],\n        ]), false);\n        $response-&gt;headers-&gt;set('Reporting-Endpoints', &quot;csp-endpoint=&quot;{$reportUri}&quot;&quot;, false);\n\n        return $response;\n    }\n}\n<\/code><\/pre>\n<p>Register it in your HTTP kernel so it runs on web requests:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">\/\/ app\/Http\/Kernel.php\nprotected $middlewareGroups = [\n    'web' =&gt; [\n        \/\/ ...\n        AppHttpMiddlewareAddCspHeaders::class,\n    ],\n];\n<\/code><\/pre>\n<h3><span id=\"Using_the_nonce_in_Blade\">Using the nonce in Blade<\/span><\/h3>\n<p>Now it\u2019s easy to tag your scripts and styles in Blade templates:<\/p>\n<pre class=\"language-php line-numbers\"><code class=\"language-php\">&lt;?php \/\/ app\/helpers.php or a Service Provider\nif (!function_exists('csp_nonce')) {\n    function csp_nonce(): string { return app('csp-nonce') ?? ''; }\n}\n<\/code><\/pre>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">&lt;script nonce=&quot;{{ csp_nonce() }}&quot; src=&quot;{{ mix('js\/app.js') }}&quot; defer&gt;&lt;\/script&gt;\n&lt;script nonce=&quot;{{ csp_nonce() }}&quot;&gt;\n    window.App = { csrf: '{{ csrf_token() }}' };\n&lt;\/script&gt;\n&lt;style nonce=&quot;{{ csp_nonce() }}&quot;&gt;\/* tiny critical CSS *\/&lt;\/style&gt;\n<\/code><\/pre>\n<p>If you\u2019re using Vite, you can customize the tags produced by the helper so the nonce is attached. One approach is to wrap the helper in your own function that prints tags with <code>nonce<\/code>. Another is to publish the Vite configuration and adjust the tag rendering. Keep the nonce on every script or style tag you output from Blade if you rely on inline bits.<\/p>\n<h3><span id=\"Dealing_with_vendor_scripts\">Dealing with vendor scripts<\/span><\/h3>\n<p>Similar to WordPress, try to keep third-party scripts external and on a short allowlist. If you absolutely need an inline snippet for a vendor, consider hashing it once it\u2019s stable, or wrap it in a small external file that\u2019s loaded with your nonce. The smaller the inline surface, the safer everything feels.<\/p>\n<h2 id=\"section-6\"><span id=\"Collecting_CSP_Reports_report-to_report-uri_and_the_Gentle_Rollout\">Collecting CSP Reports: report-to, report-uri, and the Gentle Rollout<\/span><\/h2>\n<h3><span id=\"Why_reports_matter\">Why reports matter<\/span><\/h3>\n<p>Rolling out CSP is like adjusting a soundboard. You\u2019re going to mute a few tracks you didn\u2019t expect, and the reports help you find which ones. The browser will send you a JSON payload when something is blocked, including the URL, the directive, and the source. Hook these up before enforcement and you\u2019ll save yourself days of guessing.<\/p>\n<h3><span id=\"Use_Report-Only_first_then_enforce\">Use Report-Only first, then enforce<\/span><\/h3>\n<p>Start with <code>Content-Security-Policy-Report-Only<\/code> and ship the exact policy you hope to enforce. Give it a few days, watch the reports, and fix or allow what makes sense. This is the safest, least dramatic way to go live. When the noise settles, switch to <code>Content-Security-Policy<\/code> for enforcement.<\/p>\n<h3><span id=\"report-to_and_report-uri_together\">report-to and report-uri, together<\/span><\/h3>\n<p>The reporting landscape has changed a few times, and different browsers have different preferences. I still define <code>report-to<\/code> and keep <code>report-uri<\/code> as a fallback so I capture the widest set of reports. The gist looks like this:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">Content-Security-Policy-Report-Only: ... report-to csp-endpoint; report-uri https:\/\/report.example.com\/csp\nReport-To: {&quot;group&quot;:&quot;csp-endpoint&quot;,&quot;max_age&quot;:10886400,&quot;endpoints&quot;:[{&quot;url&quot;:&quot;https:\/\/report.example.com\/csp&quot;}]}\nReporting-Endpoints: csp-endpoint=&quot;https:\/\/report.example.com\/csp&quot;\n<\/code><\/pre>\n<p>You can roll your own endpoint in Laravel (a simple route that logs JSON is fine), but I often use a hosted service to keep things tidy. Tools like <a href=\"https:\/\/report-uri.com\" rel=\"nofollow noopener\" target=\"_blank\">managed CSP reporting dashboards<\/a> make it easy to spot patterns and silence the noise.<\/p>\n<h3><span id=\"Analyze_adjust_and_roll_out_gradually\">Analyze, adjust, and roll out gradually<\/span><\/h3>\n<p>I like to ship CSP the way I ship other risky infra changes\u2014softly. If you\u2019re on Nginx, you can set a response header on a small percentage of traffic first, then increase. If you\u2019re behind a CDN, a ruleset with a conditional header is perfect. If you want a friendly playbook for gradual rollouts in general, I\u2019ve written about <a href=\"https:\/\/www.dchost.com\/blog\/en\/vpste-canary-dagitimi-nasil-tatli-tatli-kurulur-nginx-agirlikli-yonlendirme-saglik-kontrolu-ve-guvenli-rollback\/\">canary deploys with Nginx weighted routing and safe rollbacks<\/a>\u2014the same thinking applies to security headers.<\/p>\n<h2 id=\"section-7\"><span id=\"From_Inline_Chaos_to_Calm_A_Practical_Migration_Path\">From Inline Chaos to Calm: A Practical Migration Path<\/span><\/h2>\n<h3><span id=\"1_Start_with_a_clear_minimal_policy_in_Report-Only\">1) Start with a clear, minimal policy in Report-Only<\/span><\/h3>\n<p>Don\u2019t try to solve everything day one. Start with a clean baseline, include your nonce and only the origins you truly need. Add <code>'strict-dynamic'<\/code> if your app uses a loader script. Flip to Report-Only and watch what breaks in the console and in reports.<\/p>\n<h3><span id=\"2_Enqueue_and_externalize\">2) Enqueue and externalize<\/span><\/h3>\n<p>On WordPress, move all inline blocks that are longer than a few lines into proper enqueued files. Keep <code>wp_localize_script<\/code> but rely on your nonce to legitimize the inline JSON. On Laravel, drop helpers into external JS modules and pass data to the DOM via <code>data-<\/code> attributes or a single nonced inline JSON blob you trust.<\/p>\n<h3><span id=\"3_Replace_inline_handlers_with_event_listeners\">3) Replace inline handlers with event listeners<\/span><\/h3>\n<p>Pull out <code>onclick<\/code>, <code>onload<\/code>, and friends. Add listeners in your JS bundle. It\u2019s a boring refactor that pays you back forever. Your CSP gets simpler, and your code becomes easier to maintain across themes or template changes.<\/p>\n<h3><span id=\"4_Hash_the_rare_stable_inline_block\">4) Hash the rare, stable inline block<\/span><\/h3>\n<p>If there\u2019s a tiny initialization snippet that never changes, hash it once and move on. This keeps your policy tight without introducing a complicated build step. I use SHA-256 most of the time. Make sure the hash matches the exact contents of the inline block\u2014no extra spaces or line breaks.<\/p>\n<h3><span id=\"5_Document_your_choices\">5) Document your choices<\/span><\/h3>\n<p>Write down why each domain is on your allowlist, which scripts require a nonce, and how new scripts should be added. Future you (and your teammates) will thank you. I keep this in the repo alongside the header config so reviews are easy.<\/p>\n<h2 id=\"section-8\"><span id=\"NginxApacheCDN_Where_to_Set_the_Headers\">Nginx\/Apache\/CDN: Where to Set the Headers<\/span><\/h2>\n<p>You can set CSP headers at the application layer or at the edge. I usually set them in-app when using nonces, because the nonce needs to be per-request and shared with templates. That said, if your CSP is stable and doesn\u2019t use nonces or you prefer hashes, pushing it to Nginx or a CDN can be clean and fast.<\/p>\n<h3><span id=\"Nginx_example_nonce_placeholder\">Nginx example (nonce placeholder)<\/span><\/h3>\n<p>It\u2019s common to have the app emit the actual header (so the nonce is correct) and use Nginx only for <code>Report-To<\/code> or <code>cache-control<\/code> tweaks. If you must attach CSP at Nginx, consider passing the nonce from the app to Nginx via a response header and interpolating it in a sub_filter. It\u2019s a bit more advanced, but it can work in a pinch.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Pseudocode-ish, illustration only\nadd_header Report-To '{&quot;group&quot;:&quot;csp-endpoint&quot;,&quot;max_age&quot;:10886400,&quot;endpoints&quot;:[{&quot;url&quot;:&quot;https:\/\/report.example.com\/csp&quot;}]}' always;\nadd_header Reporting-Endpoints 'csp-endpoint=&quot;https:\/\/report.example.com\/csp&quot;' always;\n\n# For CSP, prefer application layer when using nonces\n<\/code><\/pre>\n<p>If you\u2019re running behind a CDN like Cloudflare, remember that some features inject their own scripts (think Rocket Loader). Either disable those or explicitly allow them. If you want a safe way to publish apps with a CDN without opening ports, I wrote a practical walkthrough on <a href=\"https:\/\/www.dchost.com\/blog\/en\/port-acmadan-yayin-nasil-mumkun-cloudflare-tunnel-zero-trust-mtls-ve-accessi-adim-adim\/\">publishing behind Cloudflare Tunnel with Zero-Trust<\/a>, and a deeper piece on <a href=\"https:\/\/www.dchost.com\/blog\/en\/origini-korumak-cloudflare-authenticated-origin-pulls-ve-mtls-ile-gercek-kaynak-dogrulamasi\/\">protecting your origin with mTLS<\/a>\u2014both pair nicely with a strong CSP.<\/p>\n<h2 id=\"section-9\"><span id=\"Common_Gotchas_and_the_Friendly_Fixes\">Common Gotchas and the Friendly Fixes<\/span><\/h2>\n<h3><span id=\"It_works_locally_but_not_in_production\">\u201cIt works locally but not in production\u201d<\/span><\/h3>\n<p>Local builds often load fewer scripts and skip CDN features. Production might have extra analytics, A\/B testing, or a CDN optimizer that injects a helper. Grep your response for <code>&lt;script&gt;<\/code> tags you didn\u2019t expect and expand your allowlist narrowly or disable the injector.<\/p>\n<h3><span id=\"My_inline_JSON_broke\">\u201cMy inline JSON broke\u201d<\/span><\/h3>\n<p>If you put JSON inside a <code>&lt;script&gt;<\/code> tag for configuration, it still counts as an inline script. Give it a nonce. If you want to be very tidy, use a MIME type like <code>application\/json<\/code> and still add the nonce. Keep it predictable and the browser is happy.<\/p>\n<h3><span id=\"I_need_eval_for_a_vendor\">\u201cI need eval for a vendor\u201d<\/span><\/h3>\n<p><code>'unsafe-eval'<\/code> is a slippery slope. If you can avoid it, do. If a vendor absolutely requires it, try to isolate it to a separate page or route, or contain it in a sandboxed iframe with its own policy. Don\u2019t add it site-wide if you can help it.<\/p>\n<h3><span id=\"Blocking_looks_random\">\u201cBlocking looks random\u201d<\/span><\/h3>\n<p>It\u2019s not random, it\u2019s just subtle. Use the browser console\u2014CSP errors are explicit about which directive failed. Then run your header through the <a href=\"https:\/\/csp-evaluator.withgoogle.com\" rel=\"nofollow noopener\" target=\"_blank\">CSP Evaluator<\/a> to catch the gotchas I still miss when I\u2019m in a hurry. Nine times out of ten, one rogue inline tag is missing a nonce.<\/p>\n<h3><span id=\"WordPress_core_or_a_plugin_still_injects_inline\">\u201cWordPress core or a plugin still injects inline\u201d<\/span><\/h3>\n<p>Many modern WordPress filters can add <code>nonce<\/code> to inline tags as shown earlier. If a plugin bypasses those APIs and prints raw HTML, nudge it into the enqueue system or open a tiny PR. As a last resort, you can pre-allow a stable inline block via a hash while you work with the vendor.<\/p>\n<h2 id=\"section-10\"><span id=\"Security_Is_a_Team_Sport_Pair_CSP_with_Good_Hygiene\">Security Is a Team Sport: Pair CSP with Good Hygiene<\/span><\/h2>\n<p>CSP is a brilliant safety net, but it\u2019s not the whole story. Keep your runtimes and plugins tidy, patch regularly, and ship boring upgrades. If you\u2019re running WordPress or Laravel on modern PHP, performance and security just feel better. I wrote a calm checklist for that journey here: <a href=\"https:\/\/www.dchost.com\/blog\/en\/php-8-x-yukseltme-kontrol-listesi-wordpress-ve-laravelde-geriye-uyumluluk-opcache-preload-ve-fpm-havuz-ayarlari-nasil-tatli-tatli-kurulur\/\">the PHP 8.x upgrade checklist for WordPress\/Laravel<\/a>. Pair that with a hardened origin and smart edge rules and you\u2019ve removed entire classes of headaches before they start.<\/p>\n<h2 id=\"section-11\"><span id=\"Wrap-Up_Your_Calm_CSP_Playbook\">Wrap-Up: Your Calm CSP Playbook<\/span><\/h2>\n<p>I still remember the first project where I flipped CSP to enforce and everything looked like it shattered. It hadn\u2019t. CSP was doing me a favor\u2014shining a light on all the places code was taking shortcuts. Once we added nonces, hashed one tiny snippet, and moved a few helpers into proper files, the site felt cleaner. Fewer mystery bugs. No surprise popups. A quiet console is a beautiful thing.<\/p>\n<p>If you\u2019re on WordPress, add a per-request nonce, push it into every script and style tag you control, and chip away at inline handlers. On Laravel, a small middleware plus a Blade helper gets you 80% there in an afternoon. For both, start with Report-Only, collect reports via <code>report-to<\/code> and a <code>report-uri<\/code> fallback, and ship the rest gradually. When in doubt, let the reports guide you instead of guesses.<\/p>\n<p>And remember, security blends best with reliability. If you want to roll out a stricter CSP the same way you would a traffic shift, take a look at how I handle <a href=\"https:\/\/www.dchost.com\/blog\/en\/vpste-canary-dagitimi-nasil-tatli-tatli-kurulur-nginx-agirlikli-yonlendirme-saglik-kontrolu-ve-guvenli-rollback\/\">canary deployments on a VPS<\/a>. If your stack lives behind a CDN, put it on a zero-trust footing with <a href=\"https:\/\/www.dchost.com\/blog\/en\/port-acmadan-yayin-nasil-mumkun-cloudflare-tunnel-zero-trust-mtls-ve-accessi-adim-adim\/\">Cloudflare Tunnel<\/a> and protect the origin itself with <a href=\"https:\/\/www.dchost.com\/blog\/en\/origini-korumak-cloudflare-authenticated-origin-pulls-ve-mtls-ile-gercek-kaynak-dogrulamasi\/\">Authenticated Origin Pulls and mTLS<\/a>. The combination of a well-tuned CSP and a calm deployment rhythm can turn even a busy WordPress or Laravel app into a trustworthy, low-drama experience.<\/p>\n<p>Hope this was helpful! If you try this playbook, I\u2019d love to hear what you bumped into and how you solved it. See you in the next post.<\/p>\n<h2 id=\"section-12\"><span id=\"Helpful_References_I_Like\">Helpful References I Like<\/span><\/h2>\n<p>When I need a quick sanity check, I hop into the MDN docs for CSP and the Evaluator tool. They\u2019re simple and to the point:<\/p>\n<ul>\n<li><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/CSP\" rel=\"nofollow noopener\" target=\"_blank\">MDN\u2019s overview of Content Security Policy<\/a><\/li>\n<li><a href=\"https:\/\/csp-evaluator.withgoogle.com\" rel=\"nofollow noopener\" target=\"_blank\">Google\u2019s CSP Evaluator<\/a><\/li>\n<li><a href=\"https:\/\/report-uri.com\" rel=\"nofollow noopener\" target=\"_blank\">Managed CSP reporting dashboards<\/a><\/li>\n<\/ul>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>So there I was, staring at a client&#8217;s WordPress dashboard at 1 a.m., wondering why the homepage was randomly breaking only for some visitors. The site still loaded, but half the buttons didn\u2019t respond. Classic ghost bug. After a quick sweep, it turned out a harmless-looking plugin slipped in a new inline script. On its [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1999,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26],"tags":[],"class_list":["post-1998","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-teknoloji"],"_links":{"self":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/1998","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/comments?post=1998"}],"version-history":[{"count":0,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/1998\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media\/1999"}],"wp:attachment":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media?parent=1998"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/categories?post=1998"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/tags?post=1998"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}