cd /blog
Web SecurityCSPXSSHTTP HeadersOWASP

#Content Security Policy: The Developer's Defence Against XSS

A deep dive into Content Security Policy (CSP) — what it is, how to craft a strict policy, and how to audit it with browser DevTools and security scanners.

3 min read 497 words

What is CSP?

Content Security Policy is an HTTP response header that tells the browser which content sources are trusted. It is the most effective defence-in-depth control against Cross-Site Scripting (XSS) attacks after input sanitisation.

Without CSP, a single XSS vulnerability lets an attacker inject arbitrary scripts. With a strict CSP, even injected scripts are blocked at the browser level.


The Anatomy of a CSP Header

Content-Security-Policy:
  default-src 'self';
  script-src  'self' 'nonce-rAndOm123';
  style-src   'self' 'unsafe-inline';
  img-src     'self' data: https:;
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
  base-uri    'self';
  form-action 'self'

Key Directives

DirectivePurpose
default-srcFallback for all resource types
script-srcJavaScript sources
style-srcCSS sources
img-srcImage sources
connect-srcFetch / XHR / WebSocket destinations
frame-ancestorsReplaces X-Frame-Options
base-uriRestricts <base> tag hijacking
form-actionWhere forms can submit to

The Nonce-Based Approach (Strict CSP)

Using 'unsafe-inline' for scripts completely undermines the XSS protection. The right approach is nonces — cryptographically random tokens generated per request.

Server-side (Node.js / Express example)

import crypto from 'crypto';
import helmet from 'helmet';

app.use((req, res, next) => {
  // Generate a cryptographically random nonce per request
  res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
  next();
});

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

Template (EJS / Handlebars)

<script nonce="<%= locals.cspNonce %>">
  // This inline script is now allowed
  console.log('Secure inline script');
</script>

Report-Only Mode: Safe Rollout

Never deploy a strict CSP straight to production. Use Report-Only mode first to catch violations without breaking anything.

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

Collect reports for 1–2 weeks, triage violations, then flip to enforcement mode.


Testing Your CSP

Browser DevTools

Open the Console tab — CSP violations are logged in red with the exact blocked resource.

Online Scanners

  • Mozilla Observatory: observatory.mozilla.org
  • CSP Evaluator: csp-evaluator.withgoogle.com
  • securityheaders.com

Command Line

# Quick header check with curl
curl -sI https://yourdomain.com | grep -i "content-security"

# Or use nikto for a broader header scan
nikto -h https://yourdomain.com

Common Pitfalls

  1. 'unsafe-inline' in script-src — Negates almost all XSS protection. Use nonces.
  2. Wildcard * in script-src — Allows any external script. Extremely dangerous.
  3. Missing frame-ancestors — Leaves you vulnerable to clickjacking.
  4. Missing base-uri<base> tag injection can redirect all relative URLs.
  5. Forgetting form-action — Attackers can redirect form submissions to their server.

Takeaways

A strict CSP is not optional for serious web applications. Pair it with:

  • Input validation and output encoding (never trust user input)
  • HttpOnly and Secure cookie flags
  • X-Content-Type-Options: nosniff
  • Referrer-Policy: strict-origin-when-cross-origin

Together these form a robust HTTP security header stack.