Skip to main content
Technology & EngineeringSecurity Practices269 lines

Content Security Policy

Configure Content-Security-Policy headers to mitigate XSS, data injection, and clickjacking attacks.

Quick Summary18 lines
You are an expert in configuring Content-Security-Policy (CSP) headers to restrict resource loading, mitigate XSS, and prevent data exfiltration in web applications.

## Key Points

1. **Start with Report-Only mode**: Deploy `Content-Security-Policy-Report-Only` first to collect violations without breaking the site.
2. **Use nonce-based script-src**: Nonces with `'strict-dynamic'` are the strongest practical CSP for modern applications.
3. **Set `object-src 'none'`**: Plugins are a major attack vector and are rarely needed.
4. **Set `frame-ancestors 'none'` or `'self'`**: This replaces `X-Frame-Options` and prevents clickjacking.
5. **Restrict `base-uri` and `form-action`**: These are often overlooked but can be exploited to redirect forms or rewrite relative URLs.
6. **Avoid `'unsafe-inline'` and `'unsafe-eval'`**: These negate much of CSP's protection. Use nonces or refactor inline code.
7. **Scope `connect-src` tightly**: Only allow the API origins your app actually calls. This limits data exfiltration if XSS occurs.
8. **Monitor violation reports**: Set up `report-uri` or `report-to` and monitor for unexpected violations — they may indicate injection attempts.
9. **Generate a new nonce per request**: Never reuse nonces. They must be cryptographically random and unique per response.
- **Adding `'unsafe-inline'` to fix broken styles**: Instead, use nonces for inline styles or move styles to external files.
- **Forgetting `'strict-dynamic'`**: Without it, scripts loaded dynamically by trusted scripts are blocked, breaking many frameworks.
- **Setting CSP in a `<meta>` tag**: The `<meta>` tag does not support `frame-ancestors`, `report-uri`, or `sandbox`. Use the HTTP header.
skilldb get security-practices-skills/Content Security PolicyFull skill: 269 lines
Paste into your CLAUDE.md or agent config

Content Security Policy — Application Security

You are an expert in configuring Content-Security-Policy (CSP) headers to restrict resource loading, mitigate XSS, and prevent data exfiltration in web applications.

Core Philosophy

Content-Security-Policy is fundamentally about shifting trust decisions from runtime to deploy time. Rather than hoping that no attacker-controlled content will ever reach the browser, CSP declares an explicit contract between the server and the browser about what resources are legitimate. Every resource that loads on the page must have been anticipated and approved by the developer who wrote the policy.

This philosophy extends beyond just blocking scripts. A well-crafted CSP encodes the application's architecture into a security boundary. It forces developers to think about where their resources come from, which third parties they depend on, and what the blast radius would be if any single origin were compromised. The discipline of writing a tight CSP often reveals unnecessary dependencies and architectural shortcuts that weaken the overall security posture.

The strongest CSP deployments treat the policy as living infrastructure, not a one-time configuration. They start in report-only mode, analyze real-world violations to tighten directives iteratively, and integrate CSP monitoring into their incident response pipeline. The goal is not a perfect policy on day one but a continuously improving contract that narrows the window of exploitability over time.

Anti-Patterns

  • Blanket unsafe-inline to "make it work": Adding 'unsafe-inline' to script-src because inline scripts break during testing effectively disables CSP's primary protection against XSS. Instead, invest the time to adopt nonces or move scripts to external files.

  • Copy-paste policies from tutorials without adaptation: Deploying a generic CSP from a blog post without auditing which origins your application actually uses leads to either an overly permissive policy that protects nothing or a broken site that blocks legitimate resources.

  • Treating CSP as a substitute for proper output encoding: CSP is a second line of defense, not a replacement for sanitizing and encoding output. An application that relies solely on CSP for XSS prevention will be vulnerable whenever the policy has gaps or is bypassed through trusted origins.

  • Allowing entire CDN domains in script-src: Whitelisting a broad CDN origin like https://cdn.jsdelivr.net permits any script hosted on that CDN to execute, including attacker-uploaded packages. Use specific paths, subresource integrity hashes, or nonce-based policies instead.

  • Deploying enforcement mode without a report-only trial period: Skipping report-only mode and going straight to enforcement risks breaking production functionality for users. Always collect and analyze violation reports before enforcing a new or tightened policy.

Overview

Content-Security-Policy (CSP) is an HTTP response header that tells the browser which sources of content (scripts, styles, images, fonts, frames, etc.) are permitted on a page. It acts as a second line of defense against XSS — even if an attacker injects a script tag, the browser blocks it if the source is not in the CSP allowlist. CSP can also prevent clickjacking, mixed content, and data exfiltration.

Core Concepts

CSP Directives

DirectiveControls
default-srcFallback for all resource types not explicitly set
script-srcJavaScript sources
style-srcCSS sources
img-srcImage sources
font-srcFont file sources
connect-srcXHR, Fetch, WebSocket endpoints
frame-srcSources allowed in <iframe>
frame-ancestorsWhich origins can embed this page (replaces X-Frame-Options)
object-srcPlugin sources (Flash, Java) — set to 'none'
base-uriRestricts <base> tag URLs
form-actionRestricts form submission targets
report-uri / report-toWhere to send violation reports

Source Values

ValueMeaning
'self'Same origin as the document
'none'Block all sources
'unsafe-inline'Allow inline scripts/styles (weakens CSP significantly)
'unsafe-eval'Allow eval() and similar dynamic code
'nonce-<base64>'Allow specific inline scripts with a matching nonce
'strict-dynamic'Trust scripts loaded by already-trusted scripts
https:Any HTTPS source
data:Data URIs
blob:Blob URIs
Specific origine.g., https://cdn.example.com

Nonce-Based CSP

A nonce is a random value generated per-request. Inline scripts are only executed if they carry a matching nonce attribute. This is the recommended approach for allowing inline scripts without 'unsafe-inline'.

Report-Only Mode

Content-Security-Policy-Report-Only enforces nothing but sends violation reports. Use it to test a policy before deploying it in enforcement mode.

Implementation Patterns

Strict Nonce-Based CSP (Express)

const crypto = require('crypto');

function cspMiddleware(req, res, next) {
  // Generate a unique nonce per request
  const nonce = crypto.randomBytes(16).toString('base64');
  res.locals.cspNonce = nonce;

  const policy = [
    `default-src 'self'`,
    `script-src 'nonce-${nonce}' 'strict-dynamic'`,
    `style-src 'self' 'nonce-${nonce}'`,
    `img-src 'self' https://cdn.example.com data:`,
    `font-src 'self' https://fonts.gstatic.com`,
    `connect-src 'self' https://api.example.com`,
    `frame-ancestors 'none'`,
    `object-src 'none'`,
    `base-uri 'self'`,
    `form-action 'self'`,
    `report-uri /api/csp-report`,
  ].join('; ');

  res.setHeader('Content-Security-Policy', policy);
  next();
}

app.use(cspMiddleware);
<!-- In templates, use the nonce on inline scripts -->
<script nonce="<%= cspNonce %>">
  // This inline script is allowed by the nonce
  initApp();
</script>

<!-- Scripts without the correct nonce are blocked -->

Helmet.js CSP (Express)

const helmet = require('helmet');

app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: [
        "'self'",
        (req, res) => `'nonce-${res.locals.cspNonce}'`,
        "'strict-dynamic'",
      ],
      styleSrc: [
        "'self'",
        (req, res) => `'nonce-${res.locals.cspNonce}'`,
      ],
      imgSrc: ["'self'", "https://cdn.example.com", "data:"],
      connectSrc: ["'self'", "https://api.example.com"],
      fontSrc: ["'self'", "https://fonts.gstatic.com"],
      objectSrc: ["'none'"],
      frameAncestors: ["'none'"],
      baseUri: ["'self'"],
      formAction: ["'self'"],
    },
    reportOnly: false,
  })
);

Django CSP with django-csp

# settings.py
MIDDLEWARE = [
    'csp.middleware.CSPMiddleware',
    # ...
]

CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'",)   # Nonces added automatically by django-csp
CSP_STYLE_SRC = ("'self'",)
CSP_IMG_SRC = ("'self'", "https://cdn.example.com", "data:")
CSP_FONT_SRC = ("'self'", "https://fonts.gstatic.com")
CSP_CONNECT_SRC = ("'self'", "https://api.example.com")
CSP_OBJECT_SRC = ("'none'",)
CSP_FRAME_ANCESTORS = ("'none'",)
CSP_BASE_URI = ("'self'",)
CSP_FORM_ACTION = ("'self'",)
CSP_INCLUDE_NONCE_IN = ['script-src', 'style-src']
CSP_REPORT_URI = '/api/csp-report'

# In templates:
# <script nonce="{{ request.csp_nonce }}">...</script>

Next.js CSP (Middleware)

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
  const response = NextResponse.next();

  const csp = [
    `default-src 'self'`,
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
    `style-src 'self' 'nonce-${nonce}'`,
    `img-src 'self' blob: data:`,
    `connect-src 'self'`,
    `font-src 'self'`,
    `object-src 'none'`,
    `frame-ancestors 'none'`,
    `base-uri 'self'`,
    `form-action 'self'`,
  ].join('; ');

  response.headers.set('Content-Security-Policy', csp);
  response.headers.set('x-nonce', nonce);

  return response;
}

CSP Violation Reporting Endpoint

app.post('/api/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
  const report = req.body['csp-report'];
  console.warn('CSP Violation:', {
    blockedUri: report['blocked-uri'],
    violatedDirective: report['violated-directive'],
    documentUri: report['document-uri'],
    sourceFile: report['source-file'],
    lineNumber: report['line-number'],
  });

  // Forward to your logging/monitoring service
  // Do not expose internal details in the response
  res.status(204).end();
});

Nginx CSP Header

server {
    listen 443 ssl;
    server_name app.example.com;

    # Static CSP (no nonce — works for apps with no inline scripts)
    add_header Content-Security-Policy
        "default-src 'self'; "
        "script-src 'self' https://cdn.example.com; "
        "style-src 'self' https://cdn.example.com; "
        "img-src 'self' data:; "
        "font-src 'self'; "
        "connect-src 'self' https://api.example.com; "
        "object-src 'none'; "
        "frame-ancestors 'none'; "
        "base-uri 'self'; "
        "form-action 'self'"
        always;
}

Best Practices

  1. Start with Report-Only mode: Deploy Content-Security-Policy-Report-Only first to collect violations without breaking the site.
  2. Use nonce-based script-src: Nonces with 'strict-dynamic' are the strongest practical CSP for modern applications.
  3. Set object-src 'none': Plugins are a major attack vector and are rarely needed.
  4. Set frame-ancestors 'none' or 'self': This replaces X-Frame-Options and prevents clickjacking.
  5. Restrict base-uri and form-action: These are often overlooked but can be exploited to redirect forms or rewrite relative URLs.
  6. Avoid 'unsafe-inline' and 'unsafe-eval': These negate much of CSP's protection. Use nonces or refactor inline code.
  7. Scope connect-src tightly: Only allow the API origins your app actually calls. This limits data exfiltration if XSS occurs.
  8. Monitor violation reports: Set up report-uri or report-to and monitor for unexpected violations — they may indicate injection attempts.
  9. Generate a new nonce per request: Never reuse nonces. They must be cryptographically random and unique per response.

Common Pitfalls

  • Adding 'unsafe-inline' to fix broken styles: Instead, use nonces for inline styles or move styles to external files.
  • Forgetting 'strict-dynamic': Without it, scripts loaded dynamically by trusted scripts are blocked, breaking many frameworks.
  • Setting CSP in a <meta> tag: The <meta> tag does not support frame-ancestors, report-uri, or sandbox. Use the HTTP header.
  • Overly permissive default-src: Setting default-src * or default-src 'self' 'unsafe-inline' 'unsafe-eval' provides almost no protection.
  • Not testing with real browsers: CSP behavior varies across browsers. Test violations in Chrome, Firefox, and Safari.
  • Allowing broad CDN origins: script-src https://cdn.jsdelivr.net allows any package on that CDN. An attacker could host malicious JS there. Use specific paths or hashes when possible.
  • Ignoring violation reports: CSP reports reveal real attacks and policy misconfigurations. Set up alerting on abnormal report volumes.

Install this skill directly: skilldb add security-practices-skills

Get CLI access →