Set Up Content Security Policy: 12 Steps, 90 Min [2026]

Cross-site scripting is still the most reported weakness class in the entire CVE database. A 2026 vulnerability statistics review put CWE-79 (XSS) at roughly 5,000-plus CVEs and a double-digit share of all disclosed flaws for the year, and a separate 2025 annual report counted more than 6,300 individual XSS-related CVEs. Output encoding and input sanitization are supposed to stop this at the source, but developers miss a spot in one template out of a thousand, and that single gap is all an attacker needs. Content Security Policy (CSP) is the header-level backstop that catches what your code review didn’t.

This tutorial walks through building a real, strict CSP from scratch: the syntax, the CSP Level 3 features that make nonce-based policies actually usable in production (strict-dynamic, Trusted Types, the Reporting API), and working configuration for Nginx, Express, Next.js, Django, Cloudflare, and WordPress. By the end you will have a policy that blocks unauthorized inline scripts, locks down where your page can load resources from, and reports violations back to you before they become incidents, not after.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

What Content Security Policy Actually Does

Content Security Policy is an HTTP response header (Content-Security-Policy) that tells the browser exactly which sources of scripts, styles, images, fonts, frames, and other resources are allowed to load on a page. Instead of trusting that every line of application code sanitized every user input correctly, CSP puts a second lock on the door at the browser level. If an attacker manages to inject a script tag through a stored XSS bug, a strict CSP simply refuses to execute it, because that script did not come from an approved source and was not signed with the correct nonce.

CSP is not a replacement for proper output encoding, parameterized queries, or a web application firewall. The OWASP Content Security Policy Cheat Sheet is explicit that CSP is a defense-in-depth control, not a primary fix for injection bugs. What it buys you is a second, independent layer that survives even when the first layer fails. That distinction matters when you are deciding how much engineering time to invest: CSP is cheap to deploy and expensive to attack around once configured correctly, but it will not save an application that has zero other security controls.

Modern CSP guidance from Google’s web.dev team leans on a specific pattern called strict CSP: a per-response cryptographic nonce combined with the strict-dynamic keyword, rather than long allow-lists of trusted domains. Allow-lists are brittle. A single compromised third-party script host, or a JSONP endpoint that can be abused to smuggle arbitrary JavaScript, defeats a host-based policy instantly. Nonce-based policies with strict-dynamic sidestep that entire category of bypass, which is why this tutorial builds toward that model rather than stopping at a basic default-src 'self' policy.

Prerequisites and Versions

You do not need specialized security tooling to follow this tutorial, but you do need administrative access to your web server or application code, and a way to inspect HTTP response headers. Confirm you have the following before starting.

  • A web server you control: Nginx 1.25+, Apache 2.4+, or a reverse proxy/CDN layer such as Cloudflare
  • Node.js 20 LTS or newer if you are implementing CSP inside an Express or Next.js application
  • Python 3.11+ and Django 5.0+ if you are implementing CSP inside a Django application
  • A current browser for testing: Chrome, Firefox, Safari, or Edge, all of which now ship baseline support for CSP Level 3 features including strict-dynamic
  • Command-line access with curl and openssl installed (both ship by default on macOS and most Linux distributions)
  • A staging environment separate from production, since you will break things on the first few attempts and that is expected
  • Optional: a WordPress 6.6+ install if you are adding CSP via a security plugin rather than server config

Budget about 90 minutes for the full implementation across writing the policy, wiring up nonces in your templates, testing in report-only mode, and switching to enforcement. If you are only adding a static policy to a simple site with no inline scripts, you can finish in under 30 minutes. The nonce-based dynamic setup for a server-rendered application with a build pipeline is the part that takes the remaining hour.

Step 1: Audit Every Resource Your Site Actually Loads

Before writing a single directive, you need a complete inventory of every script, stylesheet, font, image host, iframe, and API endpoint your site touches. Skipping this step is the single most common reason CSP rollouts fail: teams write a policy based on what they think the site loads, flip it to enforcement mode, and immediately break the checkout page because nobody remembered the payment provider’s iframe or the analytics script that was added by marketing eight months ago.

The fastest way to build this inventory is to open your production site in Chrome DevTools, go to the Network tab, reload with cache disabled, and filter by type (script, stylesheet, font, xhr, img). Record the origin of every request. Do this across your highest-traffic pages, not just the homepage. A marketing site’s product page might load a live-chat widget, a video embed, and a font CDN that the homepage never touches.

Common categories you will find in almost any production site:

  • Analytics and tag managers (Google Tag Manager, Google Analytics, Plausible, Mixpanel)
  • Payment iframes (Stripe.js, PayPal SDK, Braintree)
  • Font hosts (Google Fonts, Adobe Fonts, self-hosted woff2 files)
  • CDN-hosted JavaScript libraries (jQuery, chart libraries, video players)
  • Embedded content (YouTube, Vimeo, Google Maps, social media widgets)
  • Error tracking and session replay (Sentry, LogRocket, FullStory)
  • Your own build output (webpack or Vite-emitted chunk files, often on a separate asset subdomain)

Step 2: Understand the Core CSP Directives

CSP is built from directives, each controlling a specific resource type. default-src is the fallback that applies to any fetch directive you don’t explicitly set. connect-src, font-src, frame-src, img-src, media-src, object-src, script-src, and style-src all inherit from it if left unspecified. The table below covers the directives you’ll use in almost every real-world policy.

DirectiveControlsCommon Value
default-srcFallback for unlisted fetch directives‘self’
script-srcWhich scripts can execute‘nonce-{random}’ ‘strict-dynamic’
style-srcWhich stylesheets can load‘self’ ‘nonce-{random}’
object-srcFlash, Java applets, other plugin content‘none’
base-uriValid values for the base tag‘self’ or ‘none’
frame-ancestorsWho can embed this page in an iframe‘none’ or ‘self’
img-srcImage sources‘self’ data: https:
connect-srcfetch, XHR, WebSocket targets‘self’ api.example.com
font-srcWeb font sources‘self’ fonts.gstatic.com
upgrade-insecure-requestsRewrites http:// resource loads to https://(no value)
report-toNames a reporting endpoint group for violation reportscsp-endpoint

Two directives deserve special attention because they close off attack paths that are easy to overlook. object-src 'none' blocks legacy plugin content entirely. OWASP’s cheat sheet includes it in every baseline recommended policy because plugin content has historically been used to sidestep script restrictions. base-uri 'self' stops an attacker from injecting a base tag pointing at an attacker-controlled domain, which would otherwise silently redirect every relative URL on your page, including script and stylesheet paths, to that domain.

Step 3: Generate and Wire Up a Per-Request Nonce

A nonce is a random, single-use token that your server generates fresh for every single HTTP response and inserts both into the CSP header and into the nonce attribute of every inline script tag on that page. Because the nonce changes on every response, an attacker who injects a script tag has no way to guess or reuse the correct value, so the browser refuses to execute it. OWASP and web.dev both specify that nonces must be cryptographically random and never reused across requests. A static or predictable nonce provides essentially zero protection.

Generate a 128-bit (16-byte) random value and base64-encode it. Here is a minimal Node.js example:

const crypto = require('crypto');

function generateNonce() {
  return crypto.randomBytes(16).toString('base64');
}

module.exports = { generateNonce };

The same logic in Python, for a Django or Flask app:

import secrets
import base64

def generate_nonce() -> str:
    raw = secrets.token_bytes(16)
    return base64.b64encode(raw).decode("ascii")

Once you have a nonce generator, every inline script tag in your rendered HTML needs the matching attribute:

<script nonce="{{ csp_nonce }}">
  // This inline script executes because its nonce
  // matches the one sent in the CSP header.
  console.log('Nonce-approved script running.');
</script>

Any script tag without a matching nonce, injected by any means, gets silently blocked and logged in the browser console as a CSP violation. This is the mechanism that makes stored and reflected XSS non-executable even when the injection itself succeeds.

Step 4: Write Your First Policy in Report-Only Mode

Never deploy a new CSP directly in enforcing mode on a production site you have not audited exhaustively. Use the Content-Security-Policy-Report-Only header instead. It has identical syntax to the enforcing header, but instead of blocking violations, the browser only logs them and, if you configure reporting, sends a violation report to your endpoint. This lets you see exactly what would have broken before anything actually breaks for a real user.

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'nonce-rAnd0mVaLue123' 'strict-dynamic'; style-src 'self' 'nonce-rAnd0mVaLue123'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; upgrade-insecure-requests; report-to csp-endpoint

Leave this running in report-only mode for at least a full business cycle, a minimum of one to two weeks in production is standard guidance from teams doing staged CSP rollouts, long enough to catch a weekly cron-triggered email digest page, a monthly billing report, or a rarely visited admin panel that a shorter test window would miss entirely.

Step 5: Set Up Violation Reporting With the Reporting API

Current MDN guidance recommends the report-to directive paired with the Reporting API over the older report-uri directive, which is being phased out across browsers. report-to references a named endpoint group that you define separately with a Reporting-Endpoints header (or the older Report-To header for wider compatibility during the transition).

Reporting-Endpoints: csp-endpoint="https://reports.example.com/csp-violations"
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'nonce-rAnd0mVaLue123' 'strict-dynamic'; report-to csp-endpoint

Your reporting endpoint needs to accept POST requests with a JSON body and log them somewhere queryable. A minimal Express handler looks like this:

const express = require('express');
const app = express();

app.use(express.json({ type: ['application/json', 'application/reports+json'] }));

app.post('/csp-violations', (req, res) => {
  const reports = Array.isArray(req.body) ? req.body : [req.body];
  reports.forEach((report) => {
    console.log('CSP violation:', JSON.stringify(report));
    // In production, write this to your logging pipeline instead.
  });
  res.status(204).end();
});

app.listen(3000);

Do not skip this step to save time. Without reporting, the only way you find out your policy broke something is a support ticket from a confused user, or worse, silence while a real attack gets blocked but nobody on your team ever sees the log entry that would have told them an attacker probed the site.

Step 6: Implement CSP on Nginx

If your application is served behind Nginx, you can add the header directly in the server block. The catch is that Nginx’s built-in configuration language cannot generate a fresh random nonce per request on its own. For a truly dynamic nonce you need either an ngx_http_lua_module snippet, a small upstream application that sets the header, or a static policy for pages with no inline scripts at all.

server {
    listen 443 ssl http2;
    server_name example.com;

    add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; upgrade-insecure-requests" always;

    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}

The always flag matters. Without it, Nginx only sends the header on 2xx and 3xx responses, silently leaving 4xx and 5xx error pages unprotected. For a policy with dynamic nonces, generate the nonce in your upstream application (Node, Python, or PHP) and let Nginx pass the header through unmodified with proxy_pass_header, or add it in a Lua block using request_id-derived randomness if you are running OpenResty.

Step 7: Implement CSP in Express and Next.js

For Express applications, the Helmet middleware handles CSP header formatting for you, but you still need to generate and inject the nonce yourself per request.

const express = require('express');
const helmet = require('helmet');
const crypto = require('crypto');

const app = express();

app.use((req, res, next) => {
  res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
  next();
});

app.use((req, res, next) => {
  helmet.contentSecurityPolicy({
    useDefaults: false,
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: [`'nonce-${res.locals.cspNonce}'`, "'strict-dynamic'"],
      styleSrc: ["'self'", `'nonce-${res.locals.cspNonce}'`],
      objectSrc: ["'none'"],
      baseUri: ["'self'"],
      frameAncestors: ["'none'"],
      imgSrc: ["'self'", "data:", "https:"],
      upgradeInsecureRequests: [],
    },
  })(req, res, next);
});

In Next.js, the standard approach is to generate the nonce inside middleware.ts so it is available before any route handler or React Server Component renders, then set the header on the response and pass the nonce down through request headers so your root layout can read it and apply it to any manually inserted script tags.

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
  const csp = `default-src 'self'; script-src 'nonce-${nonce}' 'strict-dynamic'; style-src 'self' 'nonce-${nonce}'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; upgrade-insecure-requests`;

  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-nonce', nonce);

  const response = NextResponse.next({ request: { headers: requestHeaders } });
  response.headers.set('Content-Security-Policy', csp);
  return response;
}

Next.js automatically applies the nonce from the CSP header to any script it injects for its own hydration bundle when it detects a nonce present on the response, which removes most of the manual wiring for framework-generated scripts. You still need to pass the nonce explicitly to any third-party or hand-written inline script.

Step 8: Implement CSP in Django

Django does not ship CSP support in its core, so most teams use a small dedicated middleware. The pattern below generates a nonce per request, stores it on the request object so templates can access it, and adds the header on the way out.

import secrets
import base64

class ContentSecurityPolicyMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        nonce = base64.b64encode(secrets.token_bytes(16)).decode("ascii")
        request.csp_nonce = nonce
        response = self.get_response(request)
        response["Content-Security-Policy"] = (
            "default-src 'self'; "
            f"script-src 'nonce-{nonce}' 'strict-dynamic'; "
            f"style-src 'self' 'nonce-{nonce}'; "
            "object-src 'none'; base-uri 'self'; "
            "frame-ancestors 'none'; upgrade-insecure-requests"
        )
        return response

Register it in MIDDLEWARE after SecurityMiddleware, then reference the nonce as a template variable in any Django template that includes an inline script tag.

Step 9: Implement CSP on Cloudflare and WordPress

If your site sits behind Cloudflare, you can add a static CSP header through a Transform Rule without touching your origin server at all. This is useful for a quick baseline policy while you build out the nonce-based version in your application code. Cloudflare Transform Rules let you add or modify response headers based on a hostname or path match, which is enough for a default-src-and-allow-list policy, though not for per-request nonces since Cloudflare’s rule engine does not generate cryptographic randomness per request at the edge.

For WordPress, the most reliable path for most site operators is a security plugin that supports custom CSP headers, since several popular WordPress security plugins expose a CSP builder in their settings screen. WordPress core renders a large number of inline scripts and styles from plugins and themes that you do not control line-by-line, which makes a fully manual policy harder to maintain. If you manage the server directly, you can also add the header in your theme’s functions.php:

add_action('send_headers', function () {
    header("Content-Security-Policy: default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'");
});

Note the ‘unsafe-inline’ on style-src in that WordPress example. This is a pragmatic concession, not a best practice. WordPress core and most themes inject inline style attributes constantly, and removing ‘unsafe-inline’ without a nonce-per-request system will break the admin dashboard and most page builders. If you need a strict style policy on WordPress, plan for significantly more testing time than the Nginx or Express paths.

Step 10: Add Trusted Types for DOM XSS Protection

Trusted Types is a CSP Level 3 feature that closes off DOM-based XSS, a category that nonce-based script-src policies do not fully address, because DOM XSS often happens through JavaScript APIs like innerHTML or document.write rather than through injected script tags. Trusted Types forces your code to pass a typed, sanitized object to those dangerous DOM sink APIs instead of a raw string, and by 2026 this reached baseline support across current versions of Chrome, Edge, Firefox, and Safari rather than being limited to Chromium browsers as it was for several years after launch.

Content-Security-Policy: require-trusted-types-for 'script'; trusted-types default dompurify;

Once this directive is active, any code that assigns a raw string to innerHTML throws a TypeError instead of silently rendering. You then create a Trusted Types policy, typically wrapping a sanitization library such as DOMPurify, and route all HTML-string assignments through it:

if (window.trustedTypes && trustedTypes.createPolicy) {
  const policy = trustedTypes.createPolicy('default', {
    createHTML: (input) => DOMPurify.sanitize(input),
  });
  element.innerHTML = policy.createHTML(userSuppliedString);
}

Roll out Trusted Types after your script-src policy is stable and enforced. It is a second hardening pass, not a first step, because it typically surfaces a batch of legacy DOM-manipulation patterns across your codebase that need refactoring.

Step 11: Switch From Report-Only to Enforcement

Once your violation reports have gone quiet for a sustained period, meaning no new report categories showing up across several days of real production traffic on your busiest pages, you are ready to move from Content-Security-Policy-Report-Only to the enforcing Content-Security-Policy header. Do this in stages if you can: enforce on a low-traffic section of the site first, watch error rates and support tickets for 24 to 48 hours, then expand to the full site.

Keep the report-only header running in parallel with a slightly stricter draft policy even after you enforce the current one. This lets you continuously tighten the policy over time, for example testing the removal of a legacy allow-listed domain, without ever flying blind on what the next version of the policy would break.

Step 12: Verify Your Policy With Independent Scanners

After enforcement is live, run your production URL through an independent CSP scanner to catch mistakes your own testing missed. Google’s CSP Evaluator parses your live policy and flags common weaknesses, such as an accidental ‘unsafe-inline’ left in script-src, an overly broad wildcard, or a missing object-src restriction. Mozilla’s HTTP Observatory scores your overall security header posture, including CSP alongside HSTS, X-Content-Type-Options, and other headers, and gives you a letter grade you can track over time.

Run both scanners after every meaningful policy change, not just once at launch. A policy that scores well today can silently regress months later when someone adds a new third-party script tag directly to a template without updating the allow-list, and an automated scan is far more reliable than hoping someone remembers to check.

Complete Working Example: A Strict CSP for a Node/Express App

Here is a complete, working reference implementation that ties together nonce generation, the strict-dynamic script policy, Trusted Types, and reporting in a single small Express application.

const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json({ type: ['application/json', 'application/reports+json'] }));

// Generate a fresh nonce for every request.
app.use((req, res, next) => {
  res.locals.nonce = crypto.randomBytes(16).toString('base64');
  next();
});

// Build and attach the CSP header.
app.use((req, res, next) => {
  const nonce = res.locals.nonce;
  const csp = [
    "default-src 'self'",
    `script-src 'nonce-${nonce}' 'strict-dynamic'`,
    `style-src 'self' 'nonce-${nonce}'`,
    "object-src 'none'",
    "base-uri 'self'",
    "frame-ancestors 'none'",
    "img-src 'self' data: https:",
    "font-src 'self'",
    "connect-src 'self'",
    "upgrade-insecure-requests",
    "require-trusted-types-for 'script'",
    "report-to csp-endpoint",
  ].join('; ');

  res.setHeader('Reporting-Endpoints', 'csp-endpoint="https://example.com/csp-violations"');
  res.setHeader('Content-Security-Policy', csp);
  next();
});

// Reporting endpoint.
app.post('/csp-violations', (req, res) => {
  console.log('CSP violation report:', JSON.stringify(req.body));
  res.status(204).end();
});

// Example page with a nonce-approved inline script.
app.get('/', (req, res) => {
  res.send(`
    <!DOCTYPE html>
    <html>
      <head><title>CSP Demo</title></head>
      <body>
        <h1>Content Security Policy is active</h1>
        <script nonce="${res.locals.nonce}">
          console.log('This inline script executed because its nonce matched.');
        </script>
      </body>
    </html>
  `);
});

app.listen(3000, () => console.log('Listening on port 3000'));

Test it locally by requesting the page and confirming the header is present:

$ curl -sI http://localhost:3000/ | grep -i content-security-policy

content-security-policy: default-src 'self'; script-src 'nonce-k3F2z9Qp1rL8vXeW' 'strict-dynamic'; style-src 'self' 'nonce-k3F2z9Qp1rL8vXeW'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; upgrade-insecure-requests; require-trusted-types-for 'script'; report-to csp-endpoint

If you inject a test script tag into that page through a browser extension or DevTools console with no matching nonce, it will not execute, and you will see a CSP violation entry logged in your terminal from the reporting endpoint within milliseconds.

5 Common Pitfalls That Break Production Sites

These are the mistakes that show up most often when teams roll out Content Security Policy for the first time.

  • Forgetting third-party iframes. Payment providers, chat widgets, and video embeds need explicit entries in frame-src and often connect-src. Missing one silently breaks checkout, which is the worst possible page to break.
  • Reusing the same nonce across a session instead of per request. A nonce that stays constant across page loads gives an attacker a working, reusable token the moment they capture it once, which defeats the entire point of the mechanism.
  • Leaving ‘unsafe-inline’ in the policy temporarily and never removing it. Browsers ignore ‘unsafe-inline’ when a nonce or strict-dynamic is present in a compliant policy, but if your policy is malformed and falls back to a legacy interpretation, ‘unsafe-inline’ silently reopens the exact hole CSP exists to close.
  • Skipping the report-only phase to save time. Teams that jump straight to enforcement almost always break something in production within the first day, usually a page or user flow nobody tested manually.
  • Setting the header only on 2xx responses. Error pages, redirects, and API responses need the header too, especially frame-ancestors, which protects against clickjacking on every page, not just the successful ones.

Output Examples: What a Working Policy Looks Like

A minimal static policy with no dynamic nonce, suitable for a simple marketing site with no inline scripts:

Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; upgrade-insecure-requests

A CSP violation report your endpoint will receive when something is blocked, in the modern Reporting API format:

[
  {
    "age": 42,
    "type": "csp-violation",
    "url": "https://example.com/checkout",
    "body": {
      "documentURL": "https://example.com/checkout",
      "violatedDirective": "script-src",
      "effectiveDirective": "script-src",
      "originalPolicy": "default-src 'self'; script-src 'nonce-abc123' 'strict-dynamic'",
      "blockedURL": "inline",
      "statusCode": 200
    }
  }
]

That blockedURL: “inline” value combined with violatedDirective: “script-src” tells you an inline script without a valid nonce tried to run on the checkout page, exactly the signal you want to see during your report-only testing window, and exactly the signal that should alarm you if it shows up after you have switched to enforcement.

Troubleshooting: 8 Issues You Will Hit and How to Fix Them

Every CSP rollout hits some version of these issues. Here is how to diagnose and resolve each one.

SymptomLikely CauseFix
Inline scripts silently stop executingMissing or mismatched nonce attributeConfirm the nonce in the HTML tag exactly matches the one in the header for that response
Third-party widget stops renderingWidget’s own iframe or script host not allow-listedCheck the widget’s origin in the report-only log and add it to frame-src or script-src
Google Fonts stop loadingfont-src or style-src missing fonts.gstatic.com and fonts.googleapis.comAdd both hosts explicitly, or self-host the font files to avoid the dependency entirely
Browser console shows “Refused to apply inline style”style-src lacks ‘unsafe-inline’ or a matching nonce for inline style attributesAdd a nonce to style tags, or migrate inline styles to an external stylesheet
Reporting endpoint receives no reports at allReporting-Endpoints header missing or endpoint URL unreachable from the browserVerify the endpoint accepts POST and returns 2xx, and confirm the header syntax with a scanner
Policy works in Chrome but breaks in SafariSafari’s CSP3 feature support lagged behind Chromium for years and can differ by versionTest explicitly in Safari during the report-only phase, not just Chromium browsers
WordPress admin dashboard breaks after adding CSPwp-admin relies heavily on inline scripts and styles from pluginsScope the header to exclude /wp-admin/ paths, or use a nonce-aware security plugin
“Refused to frame” error on a payment pageframe-src too restrictive for the payment provider’s iframeAdd the exact iframe origin from the provider’s documentation to frame-src

Advanced Tips for a Mature CSP Deployment

Once your baseline policy is enforced and stable, a few refinements separate a good CSP from a genuinely strong one. First, avoid hash-based script-src entries for any script that changes on every build. Hashes require exact byte-for-byte matches, so they work well for a handful of static, rarely changed inline scripts but become unmanageable for a bundler-driven frontend where nonces are the far better fit.

Second, treat your CSP as living configuration that ships through the same review process as application code, not a one-time security task you check off and forget. Store the policy in version control alongside your middleware, and require a review comment explaining why any new origin was added to an allow-list, since unreviewed additions are how policies quietly drift back toward permissiveness over a year of feature work.

Third, once strict-dynamic is in place, resist the temptation to also maintain long host-based allow-lists in script-src just in case. Current CSP guidance is explicit that browsers supporting strict-dynamic ignore host-based allow-list entries in that directive entirely, so keeping them adds false confidence without adding protection, while older browsers that don’t understand strict-dynamic fall back to whatever host list you left in place. That means a lazy fallback list can quietly become the actual security boundary for a meaningful share of your traffic if you don’t audit browser usage data for your specific user base.

Finally, pair CSP with Subresource Integrity (SRI) hashes on any third-party script you load from a CDN that isn’t under your direct control. CSP restricts where a script can load from; SRI verifies that the script served from that origin hasn’t been tampered with since you last approved it. The two mechanisms solve different halves of the same supply-chain problem, and using only one leaves the other half exposed.

CSP Compared: Report-Only vs. Enforcing vs. No Policy

ConfigurationBlocks ViolationsBreaks Legacy CodeVisibility Into GapsRecommended For
No CSP headerNoNoNoneNever, on any production site
Report-OnlyNoNoFull violation logsInitial rollout, testing new directive changes
Enforcing, basic (default-src only)PartialRarelyLimited without reporting configuredSimple static sites with minimal third-party scripts
Enforcing, strict (nonce + strict-dynamic + Trusted Types)Yes, comprehensivelyOccasionally, on legacy DOM codeFull, with reporting activeAny application handling user input, authentication, or payments

How CSP Fits Into a Broader Application Security Program

CSP is one control among several that address the same underlying risk from different angles. Output encoding and input validation stop injection at the source. A web application firewall filters malicious requests before they reach your application. Static and dynamic application security testing tools catch injection bugs before code ships. CSP is what remains standing after all three of those layers miss something, which is why security teams treat it as mandatory rather than optional for any application handling authentication, payment data, or user-generated content.

The XSS CVE volume that opened this tutorial is not a sign that CSP has failed to catch on. It is a sign that the applications getting hit largely don’t have a strict policy deployed at all. Teams that have gone through the report-only-to-enforcement cycle described in this tutorial consistently report that the initial audit work from Step 1 surfaces forgotten third-party scripts and stale allow-list entries that were themselves quiet security liabilities, independent of CSP’s direct blocking effect.

If you are also working through the broader OWASP Top 10 for an application, CSP directly addresses the injection category as a mitigating control and pairs naturally with the other header-level hardening covered in OWASP’s secure headers guidance, including HSTS, X-Content-Type-Options, and Permissions-Policy.

Frequently Asked Questions

Does Content Security Policy replace the need for input sanitization?
No. CSP is a defense-in-depth control that blocks unauthorized script execution at the browser level, but it does not fix the underlying injection vulnerability in your code. You still need proper output encoding, parameterized database queries, and input validation as your primary defense; CSP is what catches the cases those defenses miss.

Will adding a strict CSP break my Google Analytics or Google Tag Manager setup?
It can, if you don’t explicitly allow-list the required domains. Google Tag Manager in particular loads additional scripts dynamically, which is exactly the pattern strict-dynamic is designed to permit safely once your base tag carries a valid nonce. Test this specific integration carefully during your report-only phase.

What is the difference between report-uri and report-to?
report-uri is the older directive that sends violation reports directly to a specified URL. report-to is the current recommended directive, which references a named endpoint group defined via the Reporting-Endpoints header and integrates with the browser’s built-in Reporting API. Current MDN guidance treats report-to as primary and report-uri as a fallback for older browsers.

Can I use CSP with a single-page application built with React or Vue?
Yes, and the nonce-based approach works well with modern SPA frameworks, provided your build tooling supports injecting a server-generated nonce into the initial HTML shell. The tricky part is usually your bundler’s code-splitting output, since dynamically loaded chunks need to inherit trust from strict-dynamic rather than requiring individual allow-list entries.

How long should I run report-only mode before switching to enforcement?
At minimum, long enough to cover every recurring workflow on your site, including monthly billing cycles or quarterly reports if you have them. Most teams run report-only for one to four weeks on high-traffic consumer sites, and longer for complex enterprise applications with many infrequently used features.

Does CSP protect against SQL injection or server-side attacks?
No. CSP is a browser-enforced, client-side control that only governs what resources a page is allowed to load and execute. It has no effect on server-side vulnerabilities like SQL injection, server-side request forgery, or authentication bypass. Those require separate controls entirely.

What happens to users on very old browsers that don’t understand CSP at all?
Browsers that don’t recognize the Content-Security-Policy header simply ignore it and load the page normally with no restrictions applied. This means CSP degrades gracefully for unsupported browsers, but also means it provides zero protection for that segment of your traffic, which is worth knowing if a meaningful share of your users are on legacy browsers.

Is Trusted Types required to have an effective CSP?
No, it’s an additional hardening layer on top of a solid script-src and style-src policy. A nonce-based strict CSP already blocks the majority of script-injection XSS. Trusted Types specifically closes the DOM-based XSS gap, where the vulnerability is in how your own JavaScript handles data rather than in an injected script tag, so it’s worth adding once your core policy is stable.

Related Coverage

Elias Virtanen

Elias Virtanen

Cybersecurity Analyst

Elias Virtanen is the Cybersecurity Analyst at Tech Insider, bringing hands-on expertise from his background in penetration testing and security consulting. He previously worked as a security researcher at F-Secure in Helsinki, where he focused on threat intelligence and vulnerability disclosure. Elias covers ransomware trends, zero-trust architecture, and the evolving regulatory landscape including NIS2 and the EU Cyber Resilience Act. He holds a CISSP certification and an MSc in Information Security from Aalto University.

View all articles