Skip to content

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.

Configure CSP globally in your τjs config:

taujs.config.ts
export default defineConfig({
security: {
csp: {
directives: {
"default-src": ["'self'"],
"script-src": ["'self'"],
"style-src": ["'self'", "'unsafe-inline'"],
"img-src": ["'self'", "data:", "https:"],
},
},
},
});

τjs automatically:

  1. Generates a unique nonce per request
  2. Adds 'nonce-<value>' to script-src if not present
  3. Applies the header to all responses
  4. Passes nonce to React’s rendering pipeline

For each request, τjs:

// Internal - τjs does this automatically
const nonce = crypto.randomBytes(16).toString('base64');
// Adds to script-src
'script-src': ["'self'", "'nonce-abc123...'"]
// Sets header
Content-Security-Policy: script-src 'self' 'nonce-abc123...'
// Passes to the renderer, in the render options
renderStream(writable, callbacks, data, location, modules, meta, signal, { cspNonce: nonce });

The nonce is automatically applied to:

  • the renderer’s own script output - React’s renderToPipeableStream nonce 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.

τ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.

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
}

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.

Override or extend CSP for specific routes:

Route directives are merged with global directives:

// Global config
security: {
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 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'"]
}
}
}
}
}

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 parameters
  • headers - Request headers
{
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
{
path: '/report',
attr: {
render: 'ssr',
middleware: {
csp: {
disabled: true // Skip route overrides, use global only
}
}
}
}

Test CSP without blocking:

export default defineConfig({
security: {
csp: {
directives: {
"default-src": ["'self'"],
"script-src": ["'self'"],
},
reporting: {
reportOnly: true,
},
},
},
});

Header sent: Content-Security-Policy-Report-Only

{
path: '/experimental',
attr: {
render: 'ssr',
middleware: {
csp: {
reportOnly: true,
directives: {
'script-src': ["'self'", "'strict-dynamic'"]
}
}
}
}
}
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"],
});
},
},
},
},
});
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);
}
}
}
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']
}
}
}
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:'
]
}
}
}
{
path: '/embed',
attr: {
render: 'ssr',
middleware: {
csp: {
directives: {
'frame-src': ["'self'", 'https://www.youtube.com'],
'frame-ancestors': ["'self'", 'https://trusted-partner.com']
}
}
}
}
}
// Start here
directives: {
'default-src': ["'self'"],
'script-src': ["'self'"],
'style-src': ["'self'"],
'img-src': ["'self'"]
}
// Add sources only when needed

2. 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-...'"]
// Too broad
'script-src': ["'self'", 'https:']
// Specific domains
'script-src': ["'self'", 'https://cdn.example.com', 'https://analytics.google.com']
reporting: {
endpoint: '/api/csp-violations',
onViolation: (report, req) => {
monitoringService.track('csp_violation', {
directive: report['violated-directive'],
blockedUri: report['blocked-uri'],
page: report['document-uri']
});
}
}

Use report-only mode initially:

security: {
csp: {
directives: { /* ... */ },
reporting: {
reportOnly: true // Monitor without blocking
}
}
}

After confirming no false positives, switch to enforce mode.