Content Security Policy
How τjs generates and manages CSP headers for your routes.
τ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 the renderer, in the render optionsrenderStream(writable, callbacks, data, location, modules, meta, signal, { cspNonce: nonce });Automatic Application
Section titled “Automatic Application”The nonce is automatically applied to:
- the renderer’s own script output - React’s
renderToPipeableStreamnonce option, Vue’s injected scripts, Solid’s hydration script window.__INITIAL_DATA__script- Client bootstrap script
You do not add nonces manually - τjs handles this.
Applying your own CSP from a hook
Section titled “Applying your own CSP from a hook”τjs manages the policy for the pages it renders. If the host applies its own CSP, or any other
response header, the hook it uses decides how reliably it is applied. Use onRequest for an
unconditional policy like this one, or preHandler when the policy depends on the route or on
authentication having succeeded. onSend reaches streamed responses as well, but a pre-byte
streaming failure gives it a second pass, so an unconditional security header belongs earlier. See
Response policy and lifecycle hooks
for the verified matrix.
Development vs Production
Section titled “Development vs Production”Development Mode
Section titled “Development Mode”If you configure no global directives at all, development falls back to a development fallback
policy (default-src, connect-src, style-src and img-src: 'self' plus data:, ws:,
http: and 'unsafe-inline', the allowances Vite’s dev server and HMR need). This fallback never
reaches production.
Whichever directives are in force - your own or the development fallback - τjs relaxes them for development on top:
// Your config, or the development fallback if you configured none{ 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”Global directives are a complete base policy in production, not a delta over the development
fallback - whatever you declare is what is sent, plus the nonce:
// Your config{ 'script-src': ["'self'"], 'style-src': ["'self'"]}
// Result in production{ 'script-src': ["'self'", "'nonce-...'"], 'style-src': ["'self'"]}If you configure no global directives at all, production sends no Content-Security-Policy
header - the development fallback never applies outside development. A route can still carry its
own CSP via middleware.csp even when no global policy exists.
Separately, when security.csp itself is absent from the config, a production boot logs a warning
to consider explicit configuration. A present but empty security.csp: {}, or one that only
configures reporting, sends no global header and logs no 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.