Skip to content
All essays
WebMarch 27, 202416 min

Content Security Policy: Complete Guide to Web Application Protection

Master Content Security Policy (CSP) to protect your web applications from XSS, clickjacking, and code injection attacks

Ü
Ümit Uz
Mobile & Full Stack Developer

Content Security Policy (CSP) is a powerful HTTP header that helps prevent cross-site scripting (XSS), clickjacking, and other code injection attacks. By defining which resources can load, CSP provides an additional layer of security for web applications.

Understanding CSP

CSP works by specifying allowed sources for content types like scripts, styles, images, and more. If a resource loads from a disallowed source, the browser blocks it.

Basic CSP Header

``http Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com


## CSP Directives

### Core Directives

``http
# Allow resources from same origin only
Content-Security-Policy: default-src 'self';

# Allow scripts from specific sources
Content-Security-Policy: script-src 'self' https://trusted.cdn.com;

# Allow stylesheets with inline styles
Content-Security-Policy: style-src 'self' 'unsafe-inline';

# Allow images from anywhere
Content-Security-Policy: img-src * data:;

# Allow fonts
Content-Security-Policy: font-src 'self' https://fonts.gstatic.com;

# Connect to APIs
Content-Security-Policy: connect-src 'self' https://api.example.com;

# Allow media
Content-Security-Policy: media-src 'self' https://media.example.com;

# Allow objects (plugins, embeds)
Content-Security-Policy: object-src 'none';

# Frame restrictions
Content-Security-Policy: frame-src 'self' https://embed.example.com;
Content-Security-Policy: frame-ancestors 'none'; # Prevent embedding

Implementing CSP

Express.js Implementation

javascript
const express = require('express');
const helmet = require('helmet');

const app = express();

// Basic CSP with helmet
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
    scriptSrc: ["'self'", "https://cdn.example.com"],
    imgSrc: ["'self'", "data:", "https:"],
    fontSrc: ["'self'", "https://fonts.gstatic.com"],
    connectSrc: ["'self'", "https://api.example.com"],
    mediaSrc: ["'self'"],
    objectSrc: ["'none'"],
    frameAncestors: ["'none'"],
    baseUri: ["'self'"],
    formAction: ["'self'"],
    frameSrc: ["'none'"],
    upgradeInsecureRequests: []
  }
}));

// Custom CSP middleware
function customCSP(directives) {
  return (req, res, next) => {
    const policy = Object.entries(directives)
      .map(([key, values]) => `${key} ${values.join(' ')}`)
      .join('; ');

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

app.use(customCSP({
  'default-src': ["'self'"],
  'script-src': ["'self'", "'unsafe-eval'", "'unsafe-inline'", "https://cdn.jsdelivr.net"],
  'style-src': ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
  'img-src': ["'self'", "data:", "https:"],
  'font-src': ["'self'", "https://fonts.gstatic.com"]
}));

Next.js Implementation

javascript
// next.config.js
const ContentSecurityPolicy = `
  default-src 'self';
  script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.jsdelivr.net;
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
  font-src 'self' https://fonts.gstatic.com;
  img-src 'self' data: blob: https:;
  connect-src 'self' https://api.example.com;
  media-src 'self';
  object-src 'none';
  frame-ancestors 'none';
`;

module.exports = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim()
          },
          {
            key: 'X-Frame-Options',
            value: 'DENY'
          },
          {
            key: 'X-Content-Type-Options',
            value: 'nosniff'
          }
        ]
      }
    ];
  }
};

Handling Inline Scripts and Styles

Nonce-Based CSP

javascript
const crypto = require('crypto');

app.use((req, res, next) => {
  // Generate nonce for inline scripts
  const nonce = crypto.randomBytes(16).toString('base64');
  res.locals.nonce = nonce;

  // Set CSP with nonce
  res.setHeader(
    'Content-Security-Policy',
    `script-src 'self' 'nonce-${nonce}' https://cdn.example.com;`
  );

  next();
});

// Template rendering
app.get('/', (req, res) => {
  res.render('index', {
    nonce: res.locals.nonce,
    message: 'Hello World'
  });
});

``html <!-- Template with nonce --> <script nonce="<%= nonce %>"> console.log('Inline script with nonce'); </script>

<script nonce="<%= nonce %>"> // jQuery or other inline code $(document).ready(function() { // ... }); </script>


### Hash-Based CSP

const crypto = require('crypto');

function generateHash(content) { return crypto .createHash('sha256') .update(content) .digest('base64'); }

// Pre-compute hashes for inline scripts const inlineScriptHashes = [ generateHash('console.log("hello")'), generateHash('alert("welcome")') ];

app.use((req, res, next) => { const hashDirective = inlineScriptHashes .map(hash => 'sha256-${hash}') .join(' ');

res.setHeader( 'Content-Security-Policy', script-src 'self' 'nonce-${res.locals.nonce}' ${hashDirective} );

next(); });


## CSP Violation Reporting

### Report-To Header

app.use((req, res, next) => { res.setHeader('Report-To', JSON.stringify({ group: 'csp-endpoint', max_age: 10886400, endpoints: [{ url: '/csp-violation-report' }] }));

res.setHeader( 'Content-Security-Policy', default-src 'self'; report-to csp-endpoint; report-uri /csp-violation-report );

next(); });

// Violation report endpoint app.post('/csp-violation-report', express.json({ type: 'application/csp-report' }), (req, res) => { const report = req.body['csp-report'];

console.error('CSP Violation:', { violatedDirective: report['violated-directive'], blockedURI: report['blocked-uri'], originalPolicy: report['original-policy'], documentURI: report['document-uri'], referrer: report['referrer'], statusCode: report['status-code'] });

// Log to monitoring service logger.error('CSP Violation', report);

res.sendStatus(204); });


### Report-Only Mode

// Test CSP without blocking app.use((req, res, next) => { // Report-only mode res.setHeader( 'Content-Security-Policy-Report-Only', default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; report-uri /csp-violation-report );

// Enforce mode (after testing) // res.setHeader( // 'Content-Security-Policy', // default-src 'self'; script-src 'self'; report-uri /csp-violation-report // );

next(); });


## Common CSP Patterns

### Strict Policy

``http
Content-Security-Policy:
  default-src 'none';
  script-src 'self';
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
  font-src 'self' https://fonts.gstatic.com;
  img-src 'self' data: https:;
  connect-src 'self' https://api.example.com;
  form-action 'self';
  frame-ancestors 'none';
  base-uri 'self';
  block-all-mixed-content;
  upgrade-insecure-requests;

Progressive Policy

``http

Start permissive

Content-Security-Policy: default-src *; script-src * 'unsafe-inline' 'unsafe-eval'

Gradually tighten

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

Eventually reach strict

Content-Security-Policy: default-src 'self'; script-src 'self'


### Development vs Production

const isDevelopment = process.env.NODE_ENV !== 'production';

const getCSP = () => { if (isDevelopment) { // Permissive for development return { 'default-src': ["'self'"], 'script-src': ["'self'", "'unsafe-eval'", "'unsafe-inline'"], 'style-src': ["'self'", "'unsafe-inline'"], 'connect-src': ["'self'", "http://localhost:*"] }; } else { // Strict for production return { 'default-src': ["'self'"], 'script-src': ["'self'", "'nonce-' + getNonce()"], 'style-src': ["'self'", "'unsafe-inline'"], 'connect-src': ["'self'", "https://api.example.com"], 'report-uri': ['/csp-violation-report'] }; } };


## Debugging CSP

### Browser DevTools

``javascript
// Console shows CSP violations
// Refused to load inline script because it violates the following CSP directive:
// "script-src 'self' 'nonce-abc123'"

Testing Tools

bash
# Test CSP with CSP Evaluator
npx csp-evaluator https://example.com

# Online tools
# https://csp-evaluator.withgoogle.com/
# https://securityheaders.com/

Best Practices

  1. 1Start with Report-Only: Test CSP before enforcing
  2. 2Use Nonce for Inline: Prefer nonce over unsafe-inline
  3. 3Minimize Directives: Only allow what you need
  4. 4Monitor Violations: Set up reporting and logging
  5. 5Gradual Tightening: Start permissive, tighten over time
  6. 6Separate Policies: Different policies for different routes
  7. 7Document Sources: Keep track of why each source is allowed
  8. 8Regular Audits: Review and update CSP regularly

Conclusion

Content Security Policy is a powerful defense against XSS and injection attacks. Implement it gradually, starting with report-only mode, and progressively tighten your policy based on violation reports. A well-configured CSP significantly improves your application's security posture.

Related essays

Next essay
CSRF Prevention: Complete Guide to Cross-Site Request Forgery Protection