LEARN · DEBUGGING GUIDE

Content Security Policy Blocking Resource: Debugging and Fixing CSP Violations

When a resource gets blocked by Content Security Policy, the browser sends a violation report and console error. This guide shows you exactly how to read those, adjust your policy, and verify the fix.

IntermediateAuth6 min read

What this usually means

The server is sending a Content-Security-Policy HTTP header (or meta tag) that restricts which origins, scripts, styles, or other resources the browser can load. When a resource doesn't match a permitted source in the policy, the browser blocks it and reports the violation. The most common causes are missing source directives for external domains, use of inline scripts without proper nonces or hashes, or overly restrictive default-src that cascades to other directives.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Open browser DevTools Console and look for red CSP error messages with 'Refused to load' and the specific directive
  • 2Check Network tab for the CSP response header on the main HTML document: `Content-Security-Policy` or `Content-Security-Policy-Report-Only`
  • 3If you have a report-uri endpoint, check the collected violation reports for blocked-uri and violated-directive
  • 4Temporarily set the policy to `Content-Security-Policy-Report-Only` to test without blocking
  • 5Use the browser's built-in CSP evaluator (Chrome: DevTools > Application > Frame > Security & Privacy) to see effective policy
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchResponse headers of the HTML document: `Content-Security-Policy`
  • searchBrowser DevTools Console for CSP error messages with line numbers
  • searchNetwork tab: filter by 'csp-report' or check report-uri endpoint logs
  • searchSource code: look for CSP meta tag `<meta http-equiv="Content-Security-Policy"`
  • searchWeb server config (nginx: add_header, Apache: Header set, or application code setting headers)
  • searchCDN or reverse proxy config that might override or strip headers
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningMissing or incorrect source in `script-src` for third-party analytics (e.g., 'https://www.google-analytics.com' not allowed)
  • warningUsing `unsafe-inline` for inline scripts when you should use a nonce or hash
  • warningCascading from `default-src` being too restrictive; `default-src 'none'` blocks everything unless explicitly allowed
  • warningForgotten report-uri or report-to directive, so violations aren't collected
  • warningPolicy applied via meta tag but overridden by HTTP header (meta tag is weaker)
  • warningWildcard `*` in `img-src` allowing images but blocking fonts or connect-src for APIs
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildAdd the blocked origin to the appropriate directive: `script-src https://example.com`
  • buildFor inline scripts, generate a unique nonce per request and add `'nonce-<base64>'` to `script-src`, then set the `nonce` attribute on the script tag
  • buildUse hash for static inline scripts: compute SHA-256 of the script content and add `'sha256-<hash>'` to `script-src`
  • buildIf using `unsafe-inline` is unavoidable, consider `strict-dynamic` for modern browsers to allow trusted scripts to load others
  • buildSet `default-src 'self'` as a baseline, then override specific directives like `script-src`, `style-src`, `img-src` as needed
  • buildEnable `Content-Security-Policy-Report-Only` first to collect violations without blocking, then transition to enforcement
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedClear browser cache and reload the page; check console for zero CSP errors
  • verifiedVerify blocked resources now load in Network tab
  • verifiedCheck report-uri endpoint for absence of new violation reports
  • verifiedUse online CSP validator like Google's CSP Evaluator to review policy
  • verifiedRun automated tests that assert no CSP violations in browser logs
  • verifiedTest in multiple browsers (Chrome, Firefox, Safari) as CSP handling slightly varies
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningUsing `unsafe-inline` broadly without understanding the security trade-off
  • warningForgetting to include `'self'` in directives when you need same-origin resources
  • warningApplying CSP via meta tag after the page has started loading resources (it should be in the <head>)
  • warningNot including a fallback `default-src` so undefined directives fallback to 'none'
  • warningHardcoding nonces in static files instead of generating them per request
  • warningIgnoring violation reports – they tell you exactly what's blocked and why
( 07 )War story

Stripe Checkout iframe Blocked by CSP

Senior Backend EngineerNode.js/Express, React, Nginx, AWS

Timeline

  1. 09:15Deploy new Stripe Checkout integration to production
  2. 09:22User reports that payment modal shows blank white screen
  3. 09:30Check browser console: 'Refused to frame 'https://checkout.stripe.com' because it violates the following Content Security Policy directive: "frame-src 'self'".'
  4. 09:35Inspect response headers: Nginx is sending `Content-Security-Policy: default-src 'self'; frame-src 'self';`
  5. 09:40Check CSP report-uri endpoint list; find multiple reports for blocked framing
  6. 09:50Temporarily change policy to `Content-Security-Policy-Report-Only` to confirm fix without blocking
  7. 10:00Update Nginx config: add `frame-src 'self' https://checkout.stripe.com;` and reload
  8. 10:05Clear cache and test: Stripe iframe loads, payment works
  9. 10:15Monitor report-uri for remaining violations; none appear

We were rolling out Stripe Checkout for our SaaS app. The integration worked perfectly in staging because our staging environment didn't have a Content Security Policy. Production had a strict CSP set in Nginx months ago for security compliance. The moment we deployed, users saw a blank modal. I opened DevTools and saw the CSP error immediately: 'Refused to frame...' because `frame-src` was locked to 'self'.

I checked the Nginx config and found the full CSP: `default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; frame-src 'self';`. That was too restrictive. I looked at the report-uri endpoint we had set up earlier—there were dozens of reports all pointing to Stripe's domain being blocked. I then changed the CSP to `Report-Only` mode to avoid breaking users while we fixed it.

I added `https://checkout.stripe.com` to the `frame-src` directive and reloaded Nginx. After clearing cache, the modal appeared. We verified by checking the console for zero errors and confirmed that the report-uri stopped receiving those violations. The lesson: always test third-party integrations against your existing CSP, and use Report-Only mode before enforcing a new policy.

Root cause

Missing `frame-src` directive for Stripe's domain; policy only allowed 'self'.

The fix

Added `frame-src 'self' https://checkout.stripe.com;` to the CSP header in Nginx.

The lesson

Always review CSP directives when integrating new external resources, and use Report-Only mode to catch violations without breaking production.

( 08 )Understanding CSP Directives and Their Fallback Chain

CSP directives like `script-src`, `style-src`, `img-src`, and `frame-src` define allowed sources for specific resource types. If a directive is not explicitly set, the browser falls back to `default-src`. If `default-src` is also not set, the behavior is permissive (no restriction). A common mistake is setting `default-src 'none'` or `default-src 'self'` without overriding specific directives, which blocks everything not explicitly allowed.

For example, `default-src 'self'` means scripts, styles, images, fonts, etc. can only come from the same origin. If you need a third-party script, you must set `script-src` separately. The fallback chain is: specific directive > `default-src` > no restriction. Use the browser's DevTools to see the effective policy after all fallbacks.

( 09 )Nonces vs. Hashes for Inline Scripts and Styles

When you have inline scripts (e.g., `<script>alert('hi')</script>`), CSP requires either `'unsafe-inline'` (insecure), a nonce, or a hash. A nonce is a random token generated per request: the server sets `script-src 'nonce-abc123'` and adds `nonce='abc123'` to the script tag. Hashes are static: compute SHA-256 of the script content and add `'sha256-<hash>'` to `script-src`. Nonces are better for dynamic scripts; hashes for static content.

A common pitfall is using a hardcoded nonce in a static HTML file—the nonce must be unique per request. If you're using a framework like React, you can generate nonces server-side and pass them to the client. For hashes, ensure the exact script content (including whitespace) matches. Tools like `csp-evaluator` can help compute hashes.

( 10 )Using CSP Report-Only Mode Safely

Before enforcing a CSP, set the header as `Content-Security-Policy-Report-Only` instead of `Content-Security-Policy`. The browser will report violations via `report-uri` or `report-to` but not block resources. This allows you to collect all violations in production without breaking functionality. After you've verified that no important resources are blocked, switch to enforcement.

However, report-only mode may not catch all issues because some resources might be loaded conditionally. Also, note that `report-uri` is deprecated in favor of `report-to` (part of Reporting API). For best compatibility, use both: `report-uri /csp-report; report-to csp-endpoint` and define the endpoint via `Report-To` header.

( 11 )Debugging CSP with Browser DevTools

In Chrome, open DevTools and go to the Console. CSP errors are shown in red with a link to the violating resource. The error includes the directive and the blocked URI. You can also see the full policy in the Application tab under 'Frame' > 'Security & Privacy'. In Firefox, similar info is in the Console and under 'Security' tab.

For a deeper dive, use the Network tab to filter by 'csp-report' to see violation reports sent to the report-uri. You can also trigger a test violation by trying to load an unallowed resource. Tools like 'CSP Tester' browser extensions can help modify the policy on the fly for testing.

Frequently asked questions

Why is my CSP blocking inline event handlers like onclick?

Inline event handlers (e.g., `<button onclick='doSomething()'>`) are considered inline scripts and are blocked unless you have `'unsafe-inline'` in `script-src`, or a nonce/hash. The recommended fix is to use `addEventListener` in an external script, or use a nonce on the script tag that sets up the event listener.

Can I use a wildcard (*) to allow all resources?

Yes, but it defeats the purpose of CSP. Wildcards allow any origin, including malicious ones. It's better to specify explicit origins. For example, `img-src *` is common for images from any CDN, but avoid using `*` for `script-src` or `frame-src` due to security risks.

How do I debug CSP in a local development environment?

Set the CSP header in your local dev server (e.g., via Express middleware or a proxy). Use `Content-Security-Policy-Report-Only` to avoid blocking resources while developing. Also, use browser DevTools console to see errors. You can also use a library like 'helmet' in Node.js to set CSP easily.

What is `strict-dynamic` and when should I use it?

`strict-dynamic` is a CSP mechanism that allows scripts loaded by a trusted script (with a nonce or hash) to load additional scripts without explicitly listing their origins. It's useful for single-page apps that dynamically inject scripts. Usage: `script-src 'strict-dynamic' 'nonce-abc123'`. Note: older browsers ignore `strict-dynamic`, so include fallbacks.

Why is my report-uri not receiving CSP violation reports?

Common reasons: the report-uri endpoint is not reachable (CORS or network issue), the CSP header doesn't include `report-uri`, or the browser blocks the report due to its own CSP. Ensure the endpoint returns a 200 response and accepts POST requests. Also, check that the report-uri origin is not blocked by the same CSP.