WritingContent Security Policy Implementation Guide for Web Applications — Clixo
6 min readcsp, content-security-policy, xss, web-security, http-headers

Content Security Policy Implementation Guide for Web Applications

A comprehensive Content Security Policy implementation guide: directives explained, how to deploy incrementally, handle inline scripts, and avoid common CSP mistakes.

Content Security Policy is one of the most powerful browser security mechanisms available to web developers, and one of the most frequently misconfigured. A correctly implemented CSP stops a large class of XSS attacks from executing even when injection is present. A poorly implemented one creates a false sense of security while leaving the application fully exposed.

This guide covers CSP from first principles through production deployment, including the deployment strategy that makes it operationally safe to roll out incrementally.

What Content Security Policy Actually 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 all directives not explicitly listed. A common starting point is default-src 'self', which restricts all resource loading to the same origin and then selectively relaxes specific categories.

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

The fastest way to break your application with a CSP is to deploy an enforcement policy before you understand your application's actual resource loading behavior. Report-only mode sends violations to a reporting endpoint without blocking anything:

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:

  1. On every request, generate a cryptographically random nonce (at least 128 bits of entropy, base64-encoded)
  2. Include it in the CSP header: script-src 'nonce-{value}'
  3. Add the nonce as an attribute to every allowed inline script: nonce="{value}"
  4. 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-src and accept that your policy will be longer
  • Use Google Tag Manager as the single entry point and add only its origin, then manage all other scripts through it (with the understanding that GTM can bypass your CSP)
  • 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:

  1. Enable report-only on staging, collect violations, adjust policy
  2. Enable enforcement on staging, verify no legitimate functionality breaks
  3. Enable report-only on production, monitor for violations from real usage patterns
  4. 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.

Building a secure web application and want a team that handles security infrastructure as part of the standard build? Start a build.