Content Security Policy
How τjs generates and manages CSP headers for your routes.
Overview
Section titled “Overview”τjs provides CSP middleware that generates nonce-based Content Security Policy headers and exposes the nonce to your rendering pipeline. This allows inline scripts to execute safely while blocking unauthorised code.
Basic Configuration
Section titled “Basic Configuration”Configure CSP globally in your τjs config:
export default defineConfig({ security: { csp: { directives: { "default-src": ["'self'"], "script-src": ["'self'"], "style-src": ["'self'", "'unsafe-inline'"], "img-src": ["'self'", "data:", "https:"], }, }, },});τjs automatically:
- Generates a unique nonce per request
- Adds
'nonce-<value>'toscript-srcif not present - Applies the header to all responses
- Passes nonce to React’s rendering pipeline
How It Works
Section titled “How It Works”Nonce Generation
Section titled “Nonce Generation”For each request, τjs:
// Internal - τjs does this automaticallyconst nonce = crypto.randomBytes(16).toString('base64');
// Adds to script-src'script-src': ["'self'", "'nonce-abc123...'"]
// Sets headerContent-Security-Policy: script-src 'self' 'nonce-abc123...'
// Passes to rendererrenderStream(res, callbacks, data, location, modules, meta, nonce);Automatic Application
Section titled “Automatic Application”The nonce is automatically applied to:
- React’s
renderToPipeableStream(via nonce option) window.__INITIAL_DATA__script- Client bootstrap script
You do not add nonces manually - τjs handles this.
Development vs Production
Section titled “Development vs Production”Development Mode
Section titled “Development Mode”τjs automatically relaxes CSP for development:
// Your config{ directives: { 'script-src': ["'self'"], 'style-src': ["'self'"] }}
// τjs adds in development{ 'script-src': ["'self'", "'nonce-...'"], 'connect-src': ["'self'", 'ws:', 'http:'], // For Vite/HMR 'style-src': ["'self'", "'unsafe-inline'"] // For hot styles}Production Mode
Section titled “Production Mode”In production, only your directives plus nonce are used:
// Your config{ 'script-src': ["'self'"], 'style-src': ["'self'"]}
// Result in production{ 'script-src': ["'self'", "'nonce-...'"], 'style-src': ["'self'"]}If you ship with development-style directives, τjs logs a warning.
Per-Route CSP
Section titled “Per-Route CSP”Override or extend CSP for specific routes:
Merge Mode (Default)
Section titled “Merge Mode (Default)”Route directives are merged with global directives:
// Global configsecurity: { csp: { directives: { 'default-src': ["'self'"], 'script-src': ["'self'"] } }}
// Route config{ path: '/embed', attr: { render: 'ssr', middleware: { csp: { directives: { 'frame-ancestors': ["'self'", 'https://trusted.com'] } } } }}
// Result for /embed{ 'default-src': ["'self'"], 'script-src': ["'self'", "'nonce-...'"], 'frame-ancestors': ["'self'", 'https://trusted.com']}Replace Mode
Section titled “Replace Mode”Replace global directives entirely:
{ path: '/widget', attr: { render: 'ssr', middleware: { csp: { mode: 'replace', directives: { 'default-src': ["'self'"], 'script-src': ["'self'", 'https://cdn.example.com'], 'style-src': ["'self'", "'unsafe-inline'"] } } } }}Dynamic CSP
Section titled “Dynamic CSP”Generate directives based on request parameters:
{ path: '/user/:id', attr: { render: 'ssr', middleware: { csp: { directives: ({ params, headers }) => ({ 'img-src': [ "'self'", `https://cdn.example.com/users/${params.id}/` ], 'connect-src': ["'self'", 'https://api.example.com'] }) } } }}Function receives:
params- Route parametersheaders- Request headers
Disabling CSP
Section titled “Disabling CSP”Hard Disable (No Header)
Section titled “Hard Disable (No Header)”{ path: '/legacy', attr: { render: 'ssr', middleware: { csp: false // No CSP header at all } }}Use when:
- Legacy HTML that can’t work with CSP
- Third-party widgets with inline scripts
Soft Disable (Keep Global)
Section titled “Soft Disable (Keep Global)”{ path: '/report', attr: { render: 'ssr', middleware: { csp: { disabled: true // Skip route overrides, use global only } } }}Report-Only Mode
Section titled “Report-Only Mode”Test CSP without blocking:
Global Report-Only
Section titled “Global Report-Only”export default defineConfig({ security: { csp: { directives: { "default-src": ["'self'"], "script-src": ["'self'"], }, reporting: { reportOnly: true, }, }, },});Header sent: Content-Security-Policy-Report-Only
Per-Route Report-Only
Section titled “Per-Route Report-Only”{ path: '/experimental', attr: { render: 'ssr', middleware: { csp: { reportOnly: true, directives: { 'script-src': ["'self'", "'strict-dynamic'"] } } } }}Violation Reporting
Section titled “Violation Reporting”Configure Reporting Endpoint
Section titled “Configure Reporting Endpoint”export default defineConfig({ security: { csp: { directives: { "default-src": ["'self'"], "script-src": ["'self'"], }, reporting: { endpoint: "/api/csp-violations", onViolation: (report, req) => { console.log("CSP violation:", { documentUri: report["document-uri"], violatedDirective: report["violated-directive"], blockedUri: report["blocked-uri"], }); }, }, }, },});Custom Violation Handler
Section titled “Custom Violation Handler”reporting: { endpoint: '/api/csp-violations', onViolation: (report, req) => { const violation = report['csp-report'];
// Log to monitoring service logger.warn({ event: 'csp_violation', documentUri: violation['document-uri'], directive: violation['violated-directive'], blockedUri: violation['blocked-uri'], userAgent: req.headers['user-agent'] });
// Alert on specific violations if (violation['blocked-uri'].includes('malicious')) { alertSecurityTeam(violation); } }}Common Patterns
Section titled “Common Patterns”Allowing CDN Assets
Section titled “Allowing CDN Assets”security: { csp: { directives: { 'default-src': ["'self'"], 'script-src': ["'self'", 'https://cdn.example.com'], 'style-src': ["'self'", 'https://cdn.example.com'], 'img-src': ["'self'", 'https://cdn.example.com', 'data:'], 'font-src': ["'self'", 'https://cdn.example.com'] } }}Analytics and Tracking
Section titled “Analytics and Tracking”security: { csp: { directives: { 'default-src': ["'self'"], 'script-src': [ "'self'", 'https://www.google-analytics.com', 'https://www.googletagmanager.com' ], 'connect-src': [ "'self'", 'https://www.google-analytics.com', 'https://analytics.google.com' ], 'img-src': [ "'self'", 'https://www.google-analytics.com', 'data:' ] } }}Embedded Content
Section titled “Embedded Content”{ path: '/embed', attr: { render: 'ssr', middleware: { csp: { directives: { 'frame-src': ["'self'", 'https://www.youtube.com'], 'frame-ancestors': ["'self'", 'https://trusted-partner.com'] } } } }}Best Practices
Section titled “Best Practices”1. Start Strict, Relax as Needed
Section titled “1. Start Strict, Relax as Needed”// ✅ Start heredirectives: { 'default-src': ["'self'"], 'script-src': ["'self'"], 'style-src': ["'self'"], 'img-src': ["'self'"]}
// Add sources only when needed2. Avoid ‘unsafe-inline’ in Production
Section titled “2. Avoid ‘unsafe-inline’ in Production”// ❌ Defeats CSP purpose'script-src': ["'self'", "'unsafe-inline'"]
// ✅ Use nonces (τjs does this automatically)'script-src': ["'self'", "'nonce-...'"]3. Be Specific with Sources
Section titled “3. Be Specific with Sources”// ❌ Too broad'script-src': ["'self'", 'https:']
// ✅ Specific domains'script-src': ["'self'", 'https://cdn.example.com', 'https://analytics.google.com']4. Monitor Violations
Section titled “4. Monitor Violations”reporting: { endpoint: '/api/csp-violations', onViolation: (report, req) => { monitoringService.track('csp_violation', { directive: report['violated-directive'], blockedUri: report['blocked-uri'], page: report['document-uri'] }); }}5. Test Before Enforcing
Section titled “5. Test Before Enforcing”Use report-only mode initially:
security: { csp: { directives: { /* ... */ }, reporting: { reportOnly: true // Monitor without blocking } }}After confirming no false positives, switch to enforce mode.