I’ve seen too many CSP implementation guides that show you a perfect policy for a perfect application and pretend everything will just work. In the real world, implementing CSP is messy. You’ll hit issues you didn’t expect. Your CMS will inject inline scripts you forgot about. That analytics tool your marketing team added last quarter? It breaks everything.

This guide is for people implementing CSP on real applications. Not demos. Not fresh create-react-app projects. The kind of application that has accumulated technical debt, third-party scripts, and “temporary” hacks that have been there for three years.

Step 1: Know What You’re Loading

Before writing a single CSP directive, you need to understand what your page actually loads. Open Chrome DevTools, go to the Network tab, reload the page, and look at everything.

Sort by type and write it down:

  • Every JavaScript file and where it comes from
  • Every external script tag (analytics, chat widgets, A/B testing)
  • Every inline <script> tag (yes, even the small ones)
  • Every stylesheet source
  • Every font source
  • Every image domain
  • Every API endpoint your JavaScript calls
  • Every iframe

Most developers are genuinely surprised by how much their page loads. You probably have scripts you didn’t know existed — added by a WordPress plugin, a CMS module, or a colleague who “just needed to test something real quick.”

Step 2: Deploy Report-Only (And Accept the Chaos)

Here’s your first CSP policy. It’s intentionally strict:

C o n d s s i f c f b f r t e c t m o o r a o e e f r y g n n a s r p n a i l - t n m e m o t u p e s - e e - - r - l t - r s c - u a t S t - s c r t a r c - e - s r c - n i t u c s r c ' s c i r u r c s ' r e ' o i r c ' e s c s s n i ' s l e t e t ' s e f l ' o l ' a y s e l ' f s r f s p - e l f ' e s ' e i P l f ' d ; l ; l / o f ' ; a f ' f c l ' ; t ' n ' s i ; a ; o ; p c : n - y ; e r - ' e R ; p e o p r o t r t - O n l y :

Add this header to your server responses. Every request to every page. Don’t be selective — you want to catch everything.

Yes, your browser console will light up like a Christmas tree. That’s the point. Every one of those violations is something you need to account for in your policy.

Step 3: Set Up the Report Endpoint

You need somewhere to send those violation reports. Here’s a minimal Node.js endpoint:

app.post('/api/csp-report',
  express.json({ type: 'application/csp-report' }),
  (req, res) => {
    const report = req.body['csp-report'];
    // Log to your preferred service
    console.error('CSP Violation:', {
      page: report['document-uri'],
      blocked: report['blocked-uri'],
      directive: report['violated-directive'],
      source: report['source-file'],
      line: report['line-number'],
    });
    res.status(204).end();
  }
);

Or if you’re using PHP:

<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_SERVER['CONTENT_TYPE'] === 'application/csp-report') {
    $report = json_decode(file_get_contents('php://input'), true);
    $csp = $report['csp-report'] ?? [];
    error_log("CSP Violation: " . json_encode([
        'page' => $csp['document-uri'] ?? '',
        'blocked' => $csp['blocked-uri'] ?? '',
        'directive' => $csp['violated-directive'] ?? '',
    ]));
    http_response_code(204);
    exit;
}

You could also use a third-party service like Report URI (free tier available) which gives you a dashboard for analyzing violations. Worth it if you don’t want to build your own.

Step 4: Analyze and Categorize

After running report-only for a few days, you’ll start seeing patterns. Group violations into categories:

Category A: Legitimate third-party scripts — Google Analytics, Stripe.js, Intercom, Hotjar, etc. These need their domains added to the appropriate directives.

Category B: Inline scripts from your own code — These need to be moved to external files or converted to use nonces.

Category C: Inline scripts from your framework/CMS — WordPress, React (in dev mode), various CMS plugins. These need special handling.

Category D: Stuff nobody remembers adding — Old A/B testing scripts, deprecated analytics pixels, “temporary” debug code. Remove these if possible.

Category E: Dynamic content — User-generated content that includes images or iframes. These need appropriate directives.

Step 5: Build Your Actual Policy

Now update your policy to account for everything you found:

C o n d s s i f c f f b f u r t e c t m o o r r a o p e e f r y g n n a a s r g p n a i l - t n m m e m r o t u p e s - e e e - - a r - l t - r s c - - u a d t S t - s c r t s a r c e - e - s r c - r n i t - u c s r c ' s c c i i r u r c s ' r e ' o n i r c ' e s c ' s s n s i ' s l e s t e e / t ' s e f l ' e o l ' c a y s e l ' f s l r f s u p - e l f ' e f s ' e r i P l f ' d l ' ; l e / o f ' a h f ' f - c l ' ' t t ' h n ' r s i ; h u a t t o ; e p c t n : p h t n q - y t s s t p e u r - p a h : t s ' e e R s f t / p : ; s p e : e t / s / t o p / - p f : s r o / i s o j ; t r w n : n / s t w l ; t w . - w i s w s O . n . w t n g e g . r l o ' s g i y o t o p : g h a o e l t t g . e t i l c t p c e o a s . - m g : c a ; m / o n a / m a n f ; l a o y g n t e t i r s c . . s c g . o o c m o o g m h l t e h t a t p p t s i p : s s / . : c / j o / s m a . ; p s i t . r s i t p r e i . p c e o . m c ; o m ;

Deploy this updated policy in report-only mode. The violation count should drop significantly. If it doesn’t, you missed something. Go back to step 1.

Step 6: Deal with Inline Scripts

This is where most people get stuck. Inline scripts are everywhere, and CSP doesn’t like them. You have three options:

Option A: Move to External Files (Best)

<!-- Before -->
<script>
  function initApp() {
    console.log('Starting app...');
  }
</script>

<!-- After (create a file: /js/app-init.js) -->
<script src="/js/app-init.js"></script>

Simple, clean, CSP-friendly. Do this for everything you can.

Option B: Use Nonces (Best for Dynamic Content)

<?php
$nonce = base64_encode(random_bytes(16));
header("Content-Security-Policy: script-src 'self' 'nonce-$nonce'");
?>
<script nonce="<?php echo $nonce; ?>">
  // This works because the nonce matches
</script>
<script>
  // This is BLOCKED — no nonce
</script>

The nonce changes on every request, so even if an attacker injects a <script> tag, they can’t predict the correct nonce.

Option C: Use ‘unsafe-inline’ (Acceptable Compromise)

s c r i p t - s r c ' s e l f ' ' u n s a f e - i n l i n e '

I know what you’re thinking: “But the whole point of CSP is to block inline scripts!” Yes, and if you can avoid unsafe-inline, you should. But if you’re dealing with a legacy WordPress site with 47 plugins that all inject inline scripts, unsafe-inline is better than no CSP at all. You still get frame-ancestors, connect-src, and default-src protections.

Step 7: Switch to Enforcement

After running report-only with near-zero violations for at least a week:

C o n d s s i f c f b f u r t e c t m o o r a o p e e f r y g n n a s r g p n a i l - t n m e m r o t u p e s - e e - - a r - l t - r s c - u a d t S t - s c r t a r c e - e - s r c - n i t - u c s r c ' s c i i r u r c s ' r e ' o n i r c ' e s c s s n s i ' s l e t e e / t ' s e f l ' o l ' c a y s e l ' f s r f s u p - e l f ' e s ' e r i P l f ' d l ; l e / o f ' a h f ' f - c l ' ' t t ' n ' r s i ; ' u a t o ; e p c n n : p h n q - y o s s t e u r : n a h : t ' e e c f t / p ; s p e e t / s t o - - p f : s r R i s o / ; t A n : n / N l ; t w D i s w O n . w M e g . ' ' s g t o h h a o t t t g t t i l p p c e s s . - : : c a / o n / m a w f ; l w o y w n t . t i g s c o . s o g . g o c l o o e g m t l ; a e g a m p a i n s a . g c e o r m . ; c o m ;

Note: I kept report-uri even in enforcement mode. This way you’ll still catch new violations when someone adds a new third-party script or makes a change that breaks the policy.

Common Mistakes I See Repeatedly

1. Using ‘unsafe-inline’ AND ‘unsafe-eval’ for script-src. If you’re doing this, you might as well not have script-src at all. Eval is one of the most commonly exploited JavaScript functions. If your app requires eval(), refactor it. There’s almost always a better way.

2. Wildcard origins. script-src * or script-src https: defeats the purpose. Be specific about which domains you allow.

3. Forgetting about service workers. If you use service workers, add worker-src 'self' to your policy.

4. Not handling the admin area separately. Your public-facing site can have a strict policy, but your CMS admin panel might need a more permissive one. That’s fine — use different policies for different paths.

5. Testing only in Chrome. Different browsers implement CSP slightly differently. Test in Firefox and Safari too.

Server Configuration Examples

Nginx

location / {
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
    try_files $uri $uri/ =404;
}

Apache

<IfModule mod_headers.c>
    Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'"
</IfModule>

PHP (in your framework or index.php)

header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'; base-uri 'self'");

Verify Everything

After deploying, check your site with headertest.com. It’ll show you exactly what CSP header your server is returning, whether it’s valid, and if there are any issues. It’s a quick sanity check before you call it done.

CSP implementation isn’t a one-time thing. You’ll be iterating on it as your application evolves. But getting that first enforcement policy live? That’s a huge security win. Stop putting it off.