Understanding Nonces in CSP
Understanding Nonces in CSP
Cryptographic nonces (numbers used once) provide a dynamic solution for allowing specific inline scripts and styles while maintaining strong CSP protection. Each nonce is a unique, randomly generated value that must match between the CSP header and the script or style tag, ensuring only authorized inline content executes.
Implementing a robust nonce system:
// Nonce Generation and Management System
const crypto = require('crypto');
class NonceManager {
constructor(options = {}) {
this.algorithm = options.algorithm || 'sha256';
this.nonceLength = options.nonceLength || 16;
this.nonceCache = new Map();
this.cacheTimeout = options.cacheTimeout || 300000; // 5 minutes
}
generateNonce() {
const nonce = crypto.randomBytes(this.nonceLength).toString('base64');
const timestamp = Date.now();
// Cache nonce with timestamp for validation
this.nonceCache.set(nonce, timestamp);
// Clean old nonces
this.cleanExpiredNonces();
return nonce;
}
validateNonce(nonce) {
if (!this.nonceCache.has(nonce)) {
return false;
}
const timestamp = this.nonceCache.get(nonce);
const age = Date.now() - timestamp;
// Nonce expires after timeout
if (age > this.cacheTimeout) {
this.nonceCache.delete(nonce);
return false;
}
// Each nonce can only be used once
this.nonceCache.delete(nonce);
return true;
}
cleanExpiredNonces() {
const now = Date.now();
for (const [nonce, timestamp] of this.nonceCache.entries()) {
if (now - timestamp > this.cacheTimeout) {
this.nonceCache.delete(nonce);
}
}
}
// Generate CSP header with nonce
generateCSPHeader(nonce, additionalDirectives = {}) {
const defaultDirectives = {
'default-src': ["'self'"],
'script-src': ["'self'", `'nonce-${nonce}'`],
'style-src': ["'self'", `'nonce-${nonce}'`],
'img-src': ["'self'", 'data:', 'https:'],
'font-src': ["'self'"],
'connect-src': ["'self'"],
'frame-ancestors': ["'none'"],
'base-uri': ["'self'"],
'form-action': ["'self'"]
};
// Merge with additional directives
const directives = { ...defaultDirectives, ...additionalDirectives };
return Object.entries(directives)
.map(([key, values]) => `${key} ${values.join(' ')}`)
.join('; ');
}
}
// Express middleware for nonce-based CSP
function createNonceMiddleware(nonceManager) {
return (req, res, next) => {
// Generate unique nonce for this request
const nonce = nonceManager.generateNonce();
// Make nonce available to templates
res.locals.nonce = nonce;
// Set CSP header with nonce
const cspHeader = nonceManager.generateCSPHeader(nonce);
res.setHeader('Content-Security-Policy', cspHeader);
// Override render to inject nonce into inline scripts
const originalRender = res.render;
res.render = function(view, options, callback) {
options = options || {};
options.nonce = nonce;
originalRender.call(this, view, options, (err, html) => {
if (err) return callback(err);
// Inject nonce into existing inline scripts
html = html.replace(/<script(?![^>]*\snonce=)/gi, `<script nonce="${nonce}"`);
html = html.replace(/<style(?![^>]*\snonce=)/gi, `<style nonce="${nonce}"`);
callback(null, html);
});
};
next();
};
}