Skip to main content
Beta: Front-End Checklist is currently in beta. Some issues are still being fixed. Thanks for your patience.

Implement a content security policy

A Content Security Policy is implemented to prevent XSS attacks and control resource loading.

Utilities
Quick take
Typical fix time 45 min
  • Start with Content-Security-Policy-Report-Only to test without breaking your site
  • Use nonces or hashes for inline scripts instead of unsafe-inline
  • Avoid unsafe-eval unless absolutely necessary
  • Set strict default-src then allow specific sources
  • Monitor CSP reports to catch violations
  • Sanitize untrusted HTML before rendering it into the DOM
  • Use Trusted Types in larger apps as defense in depth for DOM XSS sinks
Why it matters: Content Security Policy prevents cross-site scripting (XSS), clickjacking, and data injection attacks by controlling which resources can be loaded and executed on your pages. It reduces blast radius, but it does not replace output encoding and sanitization.

Rule Details

Content Security Policy (CSP) is your primary defense against XSS attacks by controlling what resources browsers can load.

Code Example

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'; form-action 'self';

Why It Matters

Content Security Policy prevents cross-site scripting (XSS), clickjacking, and data injection attacks by controlling which resources can be loaded and executed on your pages.

Sanitize Untrusted HTML Before Rendering

CSP reduces exploitability, but it should never be the only protection when you render rich text from a CMS, markdown pipeline, comments system, or WYSIWYG editor. Sanitize untrusted HTML before it reaches a DOM sink such as innerHTML or dangerouslySetInnerHTML.

import DOMPurify from 'dompurify'
 
function RichContent({ html }: { html: string }) {
  const safeHtml = DOMPurify.sanitize(html, {
    USE_PROFILES: { html: true },
  })
 
  return <div dangerouslySetInnerHTML={{ __html: safeHtml }} />
}
CSP is not a substitute for sanitization

If an application renders attacker-controlled HTML directly into the DOM, an overly broad policy, legacy browser, or later config drift can still leave exploitable XSS paths. Sanitize first, then let CSP reduce the remaining blast radius.

CSP Directives Overview

DirectiveControlsExample
default-srcFallback for all fetch directives'self'
script-srcJavaScript sources'self' 'nonce-abc123'
style-srcCSS sources'self' 'unsafe-inline'
img-srcImage sources'self' data: https:
connect-srcFetch, XHR, WebSocket'self' <trusted-api-origin>
font-srcFont file sources'self' <approved-font-origin>
frame-srciframe sources'none'
object-srcPlugins (Flash, etc.)'none'
base-uri<base> tag URLs'self'
form-actionForm submission targets'self'

Server Configuration

Next.js

// next.config.ts
import type { NextConfig } from 'next'
 
const generateCSP = () => {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
 
  const csp = [
    `default-src 'self'`,
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
    `style-src 'self' 'nonce-${nonce}'`,
    `img-src 'self' data: https:`,
    `font-src 'self'`,
    `connect-src 'self' https://api.example.com`,
    `frame-ancestors 'none'`,
    `object-src 'none'`,
    `base-uri 'self'`,
    `form-action 'self'`,
  ].join('; ')
 
  return { csp, nonce }
}
 
const config: NextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: generateCSP().csp,
          },
        ],
      },
    ]
  },
}
 
export default config

Next.js Middleware with Nonce

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
 
  const cspHeader = [
    `default-src 'self'`,
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
    `style-src 'self' 'nonce-${nonce}'`,
    `img-src 'self' blob: data: https:`,
    `font-src 'self'`,
    `object-src 'none'`,
    `base-uri 'self'`,
    `form-action 'self'`,
    `frame-ancestors 'none'`,
    `upgrade-insecure-requests`,
  ].join('; ')
 
  const response = NextResponse.next()
 
  response.headers.set('Content-Security-Policy', cspHeader)
  response.headers.set('x-nonce', nonce)
 
  return response
}

Nginx

# /etc/nginx/snippets/csp.conf
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'; form-action 'self';" always;
 
# In server block
server {
    listen 443 ssl http2;
    server_name example.com;
 
    include snippets/csp.conf;
 
    # ... rest of config
}

Apache

# .htaccess or httpd.conf
<IfModule mod_headers.c>
    Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'; form-action 'self';"
</IfModule>

Using Nonces for Inline Scripts

// app/layout.tsx
import { headers } from 'next/headers'
import Script from 'next/script'
 
export default async function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const headersList = await headers()
  const nonce = headersList.get('x-nonce') || ''
 
  return (
    <html lang="en">
      <head>
        <Script
          nonce={nonce}
          strategy="beforeInteractive"
          dangerouslySetInnerHTML={{
            __html: `
              // Your inline script here
              console.log('Loaded with nonce');
            `,
          }}
        />
      </head>
      <body>{children}</body>
    </html>
  )
}

Using Hashes for Static Inline Scripts

// Generate hash for inline script
import crypto from 'crypto'
 
const script = `console.log('Hello, CSP!');`
const hash = crypto
  .createHash('sha256')
  .update(script)
  .digest('base64')
 
// Use in CSP header
const csp = `script-src 'self' 'sha256-${hash}'`

CSP for Common Third-Party Services

# Google Analytics + Google Fonts + YouTube embeds
Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com;
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
  font-src 'self' https://fonts.gstatic.com;
  img-src 'self' data: https://www.google-analytics.com;
  connect-src 'self' https://www.google-analytics.com;
  frame-src https://www.youtube.com https://www.youtube-nocookie.com;
  object-src 'none';
  base-uri 'self';

Report-Only Mode for Testing

# Test CSP without breaking anything
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /csp-report; report-to csp-endpoint;

CSP Report Endpoint

// app/api/csp-report/route.ts
import { NextRequest, NextResponse } from 'next/server'
 
export async function POST(request: NextRequest) {
  try {
    const report = await request.json()
 
    // Log CSP violation
    console.error('CSP Violation:', JSON.stringify(report, null, 2))
 
    // Send to monitoring service
    // await sendToMonitoring(report)
 
    return NextResponse.json({ received: true })
  } catch {
    return NextResponse.json({ error: 'Invalid report' }, { status: 400 })
  }
}

Progressive CSP Implementation

Phase 1: Report-Only

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

Phase 2: Relaxed Enforcement

Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';

Phase 3: Strict Enforcement

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}' 'strict-dynamic'; style-src 'self' 'nonce-{random}'; object-src 'none'; base-uri 'self';

Trusted Types for Larger Apps

Applications with many DOM injection points can add Trusted Types as an extra guardrail around script-creating sinks:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{random}' 'strict-dynamic';
  object-src 'none';
  base-uri 'self';
  require-trusted-types-for 'script';
  trusted-types app dompurify;
// Example Trusted Types policy for a large app
const policy = window.trustedTypes?.createPolicy('app', {
  createHTML: (input) => DOMPurify.sanitize(input, {
    RETURN_TRUSTED_TYPE: true,
  }) as unknown as string,
})

Additional Security Headers

// Complete security headers
const securityHeaders = [
  {
    key: 'Content-Security-Policy',
    value: csp,
  },
  {
    key: 'X-Content-Type-Options',
    value: 'nosniff',
  },
  {
    key: 'X-Frame-Options',
    value: 'DENY',
  },
  {
    key: 'Referrer-Policy',
    value: 'strict-origin-when-cross-origin',
  },
  {
    key: 'Permissions-Policy',
    value: 'camera=(), microphone=(), geolocation=()',
  },
  {
    key: 'Strict-Transport-Security',
    value: 'max-age=31536000; includeSubDomains',
  },
]

Testing CSP

# Check CSP header
curl -I https://example.com | grep -i content-security-policy
 
# Test with browser DevTools
# Open Console - CSP violations appear as errors
# Open Network tab - check response headers

Online Tools

Common CSP Mistakes

MistakeRiskFix
unsafe-inline for scriptsXSS attacksUse nonces or hashes
unsafe-evalCode injectionAvoid eval(), new Function()
Overly permissive *Data exfiltrationSpecify exact domains
Missing object-src 'none'Flash exploitsAlways set to 'none'
Missing base-uriBase tag injectionSet to 'self'
Rendering raw untrusted HTMLDOM XSSSanitize before innerHTML
No Trusted Types on large app shellUnsafe DOM sinks remain reachableAdd require-trusted-types-for 'script' where supported
Start with Report-Only

Always test CSP in report-only mode first. A misconfigured CSP can break your entire site. Monitor reports for at least a week before enforcing.

Exceptions

  • A missing or weak header should be evaluated against the live production response path, not only the framework or server config in isolation.
  • Legacy integrations or embedded third-party content may require narrowly scoped exceptions, but they should be documented explicitly instead of left permissive by default.
  • When multiple security headers are missing, prioritize the header that removes the highest exploitability or browser capability first.

Standards

  • Align the implementation with OWASP: HTTP Headers Cheat Sheet and verify the effective response or browser behavior, not only the configuration file.
  • Align the implementation with MDN: Web security and verify the effective response or browser behavior, not only the configuration file.

Support Notes

  • The feature is supported across the current project browser matrix.
  • Baseline-compatible minimums: chrome 115, edge 115, firefox 116, safari 16.4, safari_ios 16.4.
  • Add a fallback or a narrower policy note when a required project target falls outside that support range.

Verification

Automated Checks

  • Test the affected flow in a production-like environment, not just local development.
  • Document any intentional exceptions explicitly.

Manual Checks

  • Inspect the final HTTP response or browser behavior to confirm the control is actually enforced.
  • Verify third-party integrations or embeds still work after the restriction is applied.
  • Attempt one representative rich-content render and confirm the HTML is sanitized before insertion.
  • In larger apps, confirm Trusted Types violations appear during testing when unsafe DOM sinks are used.

Use with AI

Copy these prompts to use with your AI assistant, or install the MCP server to use directly from Claude, Cursor, or Windsurf.

Check

Verify implementation

Check if this website implements a Content Security Policy header and analyze its directives. Also review any use of `innerHTML`, `dangerouslySetInnerHTML`, HTML template injection, and Trusted Types enforcement for large applications.

Fix

Auto-fix issues

Implement a strict Content Security Policy to prevent XSS attacks and control resource loading. Sanitize untrusted HTML before rendering and add Trusted Types enforcement where the application has enough DOM injection surface to benefit from it.

Explain

Learn more

Explain how CSP provides a security layer that helps detect and mitigate XSS and data injection attacks, and why sanitization is still required before rendering untrusted HTML.

Review

Code review

Review server config, headers, forms, and integration points related to Implement a content security policy. Flag exact responses, cookies, or browser behaviors that violate the rule, and verify them against the effective production-like response. Inspect HTML injection sinks and note whether Trusted Types or sanitization protects them.

Sources

References used to support the guidance in this rule.

Further Reading

Tools and supplementary material for exploring the topic in more depth.

Mozilla Observatory
observatory.mozilla.orgTool

Rules that often go hand-in-hand with this one.

Set an X-Frame-Options header

The X-Frame-Options header controls whether your page can be embedded in an iframe, frame, or object — preventing clickjacking attacks.

Security
Set a Referrer-Policy header

The Referrer-Policy header controls how much referrer information is sent when navigating from your site to another, protecting user privacy and preventing leaking sensitive URL parameters.

Security
Set X-Content-Type-Options: nosniff

The X-Content-Type-Options: nosniff header prevents browsers from MIME-sniffing a response away from the declared Content-Type, blocking a class of drive-by download and XSS attacks.

Security

Was this rule helpful?

Your feedback helps improve rule quality. This stays internal for now.

Loading feedback...
0 / 385