How to Implement a Proper Content Security Policy (CSP) to Prevent XSS

A Content Security Policy (CSP) prevents XSS attacks by serving an HTTP response header that whitelists trusted resource origins and blocks unauthorized inline script execution, effectively neutralizing malicious code injections.

To implement a proper Content Security Policy (CSP) and prevent XSS vulnerabilities in your web applications, you must configure strict resource loading rules that browsers enforce at runtime. The thedaviddias/Front-End-Checklist repository flags CSP implementation as a medium-priority security requirement in its README.md, emphasizing that developers should move beyond basic headers to adopt nonce-based policies for maximum protection.

Understanding CSP Architecture for XSS Prevention

A Content Security Policy functions as a declarative allowlist that restricts which scripts, styles, and resources a browser may load or execute. Unlike reactive XSS filters, CSP operates proactively by blocking unauthorized resource requests before they reach the DOM, making it one of the most effective defense-in-depth mechanisms against injection attacks.

According to the Front-End Checklist source code, implementing CSP correctly requires four architectural steps: defining a restrictive baseline, eliminating unsafe inline code, serving the policy via HTTP headers, and iteratively testing configurations.

Essential CSP Directives for XSS Mitigation

The following directives form the foundation of an XSS-resistant policy:

Directive Purpose Recommended Value
default-src Fallback for unspecified resource types 'self'
script-src Controls JavaScript execution sources 'self' 'nonce-<random>'
style-src Restricts CSS loading and inline styles 'self' 'nonce-<random>'
object-src Blocks potentially dangerous plugins 'none'
base-uri Restricts the base element 'self'
frame-ancestors Prevents clickjacking via framing 'self'
upgrade-insecure-requests Forces HTTPS for all resource loads (no value needed)

Setting object-src 'none' is critical because it disables Flash and other plugin content that attackers commonly exploit for XSS vectors.

Step-by-Step Implementation Strategy

Define a Baseline Policy

Start with the most restrictive default possible using default-src 'self', which permits resources only from your own origin. Explicitly override this default only for specific resource types you absolutely need, such as external fonts or analytics scripts. This approach follows the principle of least privilege recommended in the Front-End Checklist security guidelines.

Eliminate Inline Scripts and Styles

Move all JavaScript and CSS into external files to enable strict script-src 'self' and style-src 'self' directives. When inline code is unavoidable, implement nonce-based exceptions by generating a cryptographically random token on each server request and injecting it into both the CSP header and the corresponding HTML tags.

A nonce appears as 'nonce-abc123' in the header and nonce="abc123" in the HTML attribute. Browsers execute only inline blocks matching the current page's nonce, rendering injected attacker scripts inert.

Serve Policies via HTTP Headers

Always transmit CSP through the Content-Security-Policy HTTP response header rather than the meta tag equivalent. As noted in the Front-End Checklist analysis, HTTP headers cannot be overridden by attacker-injected meta tags, providing stronger security guarantees.

Configure your web server or application middleware to append this header on every response, including error pages that might otherwise serve as XSS vectors.

Test and Iteratively Tighten

Deploy your initial policy in Report-Only mode using the Content-Security-Policy-Report-Only header to monitor violations without breaking functionality. Use tools like securityheaders.io and Mozilla Observatory to validate your configuration, and check browser console warnings for specific blocked resource violations before enforcing the policy.

Code Implementation Examples

Express.js with Nonce Generation

In server.js or your main application file, implement middleware that generates per-request nonces:

const crypto = require('crypto');

app.use((req, res, next) => {
  // Generate cryptographically secure nonce
  const nonce = crypto.randomBytes(16).toString('base64');
  res.locals.cspNonce = nonce;
  
  // Construct strict CSP header
  const csp = [
    "default-src 'self'",
    `script-src 'self' 'nonce-${nonce}'`,
    `style-src 'self' 'nonce-${nonce}'`,
    "object-src 'none'",
    "base-uri 'self'",
    "frame-ancestors 'self'",
    "upgrade-insecure-requests"
  ].join('; ');
  
  res.setHeader('Content-Security-Policy', csp);
  next();
});

Then inject the nonce into your EJS or Pug templates:

<style nonce="<%= cspNonce %>">
  body { font-family: system-ui; }
</style>

<script nonce="<%= cspNonce %>">
  console.log('This inline script executes because the nonce matches');
</script>

Nginx Configuration

For static sites, add the header in your nginx.conf or site configuration:

server {
  listen 443 ssl;
  server_name example.com;
  
  add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' https://fonts.googleapis.com; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; upgrade-insecure-requests";
  
  location / {
    root /var/www/html;
  }
}

HTML Meta Tag Fallback

Use this method only when server header configuration is impossible, as it offers weaker protection:

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'">

Validating Your CSP Implementation

According to the Front-End Checklist repository, which validates external security links via .github/workflows/links-checker.yml, you should verify your implementation using multiple tools:

  • Visit securityheaders.io and scan your URL for a green CSP rating
  • Use Mozilla Observatory to receive a quantitative security score
  • Open browser DevTools and monitor the Console tab for Content Security Policy violation messages indicating blocked resources
  • Review server logs for CSP reports if you configured a report-uri or report-to directive

Summary

  • Content Security Policy is an HTTP header that prevents XSS by controlling resource loading and script execution
  • Implement default-src 'self' as a restrictive baseline and explicitly whitelist only necessary external origins
  • Eliminate inline scripts or secure them with cryptographic nonces that change per request
  • Always serve CSP via HTTP headers rather than meta tags to prevent attacker override
  • Test configurations using securityheaders.io, Mozilla Observatory, and browser console validation before enforcing strict policies
  • Reference the README.md in the thedaviddias/Front-End-Checklist repository for ongoing security best practices

Frequently Asked Questions

What is the difference between CSP and XSS filters like X-XSS-Protection?

CSP is a whitelist-based preventive control that stops malicious scripts from executing entirely, while legacy XSS filters attempt to sanitize detected attacks reactively. Modern browsers have deprecated X-XSS-Protection due to bypass vulnerabilities, whereas CSP remains the industry standard for XSS mitigation as recommended by the Front-End Checklist security guidelines.

Can I use CSP with inline event handlers like onclick?

No, strict CSP policies block inline event handlers entirely. You must move all event binding into external JavaScript files or use the script-src 'unsafe-inline' directive (not recommended). For inline scripts that cannot be externalized, use nonces or hashes in your script-src directive to authorize specific code blocks while blocking attacker injections.

How do I handle third-party scripts like Google Analytics or Stripe?

Add the specific HTTPS origin to your script-src directive, such as script-src 'self' https://www.google-analytics.com https://js.stripe.com. Avoid using wildcards (https:) as they allow any HTTPS origin to execute scripts. For enhanced security, request that third-party providers support Subresource Integrity (SRI) hashes and include those hashes in your policy.

Should I use Report-Only mode in production?

Initially, yes. Deploy Content-Security-Policy-Report-Only for several weeks to collect violation reports without breaking functionality. Once you confirm no legitimate resources are blocked, switch to the enforcing Content-Security-Policy header. Maintain a reporting endpoint to detect CSP bypass attempts or policy gaps in production environments.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →