CSP implementation · Practical guide
How to roll out Content Security Policy with Report-Only
Use Content-Security-Policy-Report-Only to observe a candidate before it blocks resources. Start with one representative route, identify required dependencies, test the policy in a browser and then introduce enforcement. This workflow covers the header, HTML templates, reporting and deployment decisions that must agree.
Observe one page before enforcing its policy
Choose a staging page with a form and an inline script. Record any existing enforced policy, then send the candidate below as an HTTP response header. It would restrict resources to the same origin and reject inline scripts. Report-Only observes those violations; an existing enforced policy still applies.
Use DevTools Console for this first local check. No collection endpoint is configured, so expect no server reports; the browser may also warn about the missing endpoint. HeaderScan will still report missing enforcement when only Report-Only is present. It follows up to five redirects without cookies or JavaScript and redacts full CSP values.
# Candidate HTTP response header for a staging page
Content-Security-Policy-Report-Only: default-src 'self'; object-src 'none'; base-uri 'self' - Open Network, reload and select the final document request. Confirm its status, URL and Report-Only header before investigating violations.
- Exercise the form and inline-script feature. Observe the violated directive and resource in Console; confirm this candidate did not prevent the feature from running.
- Record each required dependency and its owner. Keep this candidate in Report-Only while implementing the allowances below.
Reference: MDN: Content-Security-Policy-Report-Only
Build a dependency inventory from real journeys
Include signed-in pages, consent choices, payment flows and lazy-loaded features. For each resource, record the route, action, origin, directive and business reason. A script may load successfully while its analytics request fails connect-src; allowing the script origin alone does not cover every request.
Reproduce unexpected violations in a clean browser profile with extensions disabled. Extension-injected code and abandoned integrations should not automatically become allowed sources. Group repeat findings by route, directive and resource, then fix the dependency or approve a narrow allowance. Zero reports do not prove coverage of an untested journey.
| Dependency | Policy to review | Concrete check |
|---|---|---|
| External script | script-src or an explicit script-src-elem | Approve the required origin and verify its feature. |
| API or analytics request | connect-src | Submit the form and inspect the network response. |
| Styles, fonts and images | style-src, font-src and img-src | Check delayed content and all supported page states. |
| Embedded payment frame | frame-src | Complete the integration in its test environment. |
Reference: MDN: CSP directives and deployment
Generate the nonce once and use it in both places
Save this standalone PHP 8 example as a staging demonstration page served by PHP. random_bytes(16) generates 128 cryptographically random bits; Base64 encoding makes the value suitable for the header and HTML attribute. The same value reaches both places in this response. For a framework application, integrate generation into its response middleware and trusted template instead. Never add a nonce indiscriminately to user-supplied script markup.
Cache-Control: no-store applies to this demonstration response to avoid reusing its nonce. It is not a global policy for static assets. Reload the page and check window.appReady in Console, then compare the actual document header with the response HTML.
Check the response source and header together, then request another response and confirm the nonce changes. A CDN must not combine a fresh header with cached HTML or reuse nonce-bearing HTML across visitors. For static cached pages, consider hashes for fixed scripts instead. Update those hashes when script content changes.
<?php
$nonce = base64_encode(random_bytes(16));
header("Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-{$nonce}'; object-src 'none'; base-uri 'self'");
header('Cache-Control: no-store');
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>CSP nonce demonstration</title>
</head>
<body>
<p>Inspect the document headers and this script in DevTools.</p>
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>">window.appReady = true;</script>
</body>
</html> Reference: W3C CSP: nonce requirements and reuse
Connect reporting only after the collector works
Provision an HTTPS collector that accepts browser POSTs without an interactive login. Reporting API requests use application/reports+json; legacy report-uri requests use application/csp-report. Verify both payload formats if supporting both. Treat incoming reports as untrusted, limit request sizes and restrict access to logs containing page URLs.
Replace example.com and the collection path below, then append the reporting directives to your complete candidate policy, including any nonce. report-to selects the named Reporting-Endpoints destination. The deprecated report-uri provides a fallback for browsers without report-to support; browsers supporting report-to ignore report-uri. Deliberately trigger a staging violation and confirm collection, since delivery can be delayed or unavailable.
# Example only: these URLs must point to your working HTTPS collector
Reporting-Endpoints: csp="https://example.com/csp-reports"
Content-Security-Policy-Report-Only: default-src 'self'; object-src 'none'; base-uri 'self'; report-to csp; report-uri /csp-reports Reference: MDN: report-to and reporting compatibility
Enforce a tested candidate with a reversible deployment
Assign one configuration owner and retain the previous policy. Test enforcement on the staging route, including an intentionally unapproved inline script in its HTML. The approved script should run and the unapproved one should be blocked. Deploy a small route group, monitor real journeys and expand coverage deliberately.
If a legitimate feature breaks, restore the previous enforced policy at every layer that supplies it and keep the candidate in Report-Only for diagnosis. A second permissive policy cannot cancel an existing restrictive one. Refresh affected cached responses, preserve nonce/header alignment and verify the public response again. curl checks delivered headers, not browser enforcement.
# macOS/Linux: inspect the response after deployment or rollback
curl -q -sS -L --max-redirs 5 --max-time 30 \
-D - -o /dev/null 'https://example.com/' Reference: MDN: multiple CSP policies
Keep reading
Related guides
HTTP security headers: what to check first
Fix security header findings with CSP examples, a staged HSTS rollout, framing rules and practical browser and curl checks.
Read guide CachingCache-Control explained: no-cache, no-store and private
Choose Cache-Control for account pages, public HTML and versioned assets, then verify ETag revalidation and CDN behaviour with practical checks.
Read guide CookiesSecure, HttpOnly and SameSite cookies explained
Fix SameSite and session-cookie findings with Set-Cookie examples, host-only scope and browser checks for login, embedded content and logout.
Read guide Nginx configurationHow to configure security headers in Nginx
Add Nginx security headers to the correct HTTPS server, fix location inheritance and duplicates, then verify normal pages and error responses.
Read guide LaravelHow to add security headers in Laravel
Add response headers with Laravel middleware, register them correctly, test normal and error responses, and check proxy or CDN overrides.
Read guideHTTP response headers reference: security, cookies and caching
Look up common HTTP response headers, their purpose and the checks to make before changing a policy.
Open reference