Content Security Policy Implementation Guide for Web Applications
CSP directives, report-only rollout, inline script nonces, third-party resources, and production violation reporting.
Content Security Policy (CSP) tells a browser which resources it may load and which scripts it may execute. A carefully configured policy can reduce the impact of cross-site scripting; an overly permissive policy provides less protection.
The implementation steps below cover directives, inline scripts, violation reports, and a staged move from reporting to enforcement.
What Content Security Policy Does
When your server sends a CSP header, it instructs the browser about which sources are allowed to load each type of resource — scripts, stylesheets, fonts, images, frames, and so on. If an injected script tag points to an unlisted source, the browser refuses to load it. If inline JavaScript is not explicitly permitted, the browser refuses to execute it.
The key limitation to understand: CSP is a browser enforcement mechanism. It does not prevent the server from receiving malicious input, and it does not protect non-browser clients (API consumers, server-to-server calls). It is a defense-in-depth layer that reduces the impact of XSS, not a primary input validation defense.
Core CSP Directives
script-src
This is the most critical directive. It controls which sources can execute JavaScript. Common values:
- A specific origin:
https://cdn.example.com 'self': scripts loaded from the same origin as the page'nonce-{random}': a cryptographically random nonce included in both the CSP header and allowed inline script tags'strict-dynamic': allows scripts loaded by trusted scripts to load additional scripts, enabling module bundler patterns
Avoid 'unsafe-inline' — it permits all inline script execution and nullifies most XSS protection. Avoid 'unsafe-eval' — it permits eval() and related functions.
default-src
Sets the fallback for fetch directives that are not explicitly listed. Other directives, including frame-ancestors, need their own policy. CSP frame-ancestors reference.
object-src and base-uri
Always set object-src 'none' unless your application explicitly uses plugins. Flash and other plugin content have historically been a significant XSS vector.
Set base-uri 'self' or base-uri 'none' to prevent attackers from injecting a base tag that redirects all relative URLs.
form-action
Restricts where forms can submit. Set this explicitly to prevent form hijacking even if an attacker can inject a form element.
frame-ancestors
Controls who can embed your page in an iframe. This is the CSP replacement for the X-Frame-Options header. Setting frame-ancestors 'self' or frame-ancestors 'none' prevents clickjacking.
Content Security Policy Implementation Strategy
Start with report-only mode
An enforcement policy can block legitimate resources if those resources have not been accounted for. Start with report-only mode to collect violations without blocking them:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /csp-violations
Run report-only mode against your staging environment first, then production. Collect violation reports for a week or two before writing your enforcement policy. Every violation report tells you about a resource your policy does not currently allow.
Implement a reporting endpoint
Your reporting endpoint receives JSON POST requests with violation details: the blocked URL, the violated directive, the source file and line number. Log these and review them. In production, filter out noise from browser extensions before acting on violations.
Hosted reporting services (Report URI, Sentry) can handle the volume and provide dashboards. A simple Express or Next.js API route works for low-volume applications.
Handle inline scripts without unsafe-inline
Inline scripts are the hardest part of CSP adoption in practice, because most applications have them — analytics snippets, configuration objects embedded in HTML, third-party tag manager integrations.
The nonce approach:
- On every request, generate a cryptographically random nonce (at least 128 bits of entropy, base64-encoded)
- Include it in the CSP header:
script-src 'nonce-{value}' - Add the nonce as an attribute to every allowed inline script:
nonce="{value}" - Scripts without the correct nonce are blocked
The nonce must be unique per request, not per page load or application restart. Reusing nonces defeats their purpose.
Handle third-party scripts
Analytics, intercom, Stripe, HubSpot, Google Tag Manager — most non-trivial applications pull in third-party scripts. Each one needs a CSP entry, and each one may in turn load additional scripts from their own origins.
Options:
- Add each origin explicitly to
script-srcand accept that your policy will be longer - For Google Tag Manager, use the documented nonce setup and allow the resources required by each tag; allowing only the GTM origin does not cover every tag. Google Tag Manager CSP guide.
- Self-host critical third-party scripts from your own origin and update them via your deployment process
'strict-dynamic' allows already-trusted scripts (those with a valid nonce) to dynamically load additional scripts, which is necessary for some bundler patterns and module loading. It narrows the attack surface compared to an explicit allowlist of external origins.
Avoid common CSP mistakes
Wildcard origins: script-src *.example.com is far less restrictive than it appears if any subdomain serves user-uploaded content or can be misconfigured.
Reporting-only forever: Violation reports are useful, but the goal is enforcement. Set a deadline for moving to enforcement mode.
Overly broad data: URIs: img-src data: is commonly needed for base64-encoded images, but script-src data: should never be set — it permits executing scripts embedded as data URIs.
Not testing after changes: Any change to your application that adds a new external resource, inline script, or dynamic import may violate your CSP. Include CSP violation checking in your staging deployment process.
Deploying to Production
Roll out enforcement incrementally:
- Enable report-only on staging, collect violations, adjust policy
- Enable enforcement on staging, verify no legitimate functionality breaks
- Enable report-only on production, monitor for violations from real usage patterns
- Enable enforcement on production with violation reporting still active
Keep violation reporting enabled in production indefinitely. New features, third-party script updates, and browser behavior changes can generate violations at any time.