48 lines
1.4 KiB
TypeScript
48 lines
1.4 KiB
TypeScript
'use strict';
|
|
|
|
import type { Req, Res, Next } from '../types/http';
|
|
|
|
const crypto = require('crypto');
|
|
|
|
/** Generates a secure nonce for CSP policy. Call once per request. */
|
|
function getCspNonce(req: Req): string {
|
|
if (!req.cspNonce) {
|
|
req.cspNonce = crypto.randomBytes(16).toString('base64');
|
|
}
|
|
return req.cspNonce;
|
|
}
|
|
|
|
/**
|
|
* Applies baseline security response headers on every request.
|
|
* (See original notes on why CSP uses no nonce — index.html is static.)
|
|
*/
|
|
function securityHeaders(req: Req, res: Res, next: Next): void {
|
|
const cspPolicy =
|
|
`default-src 'self'; ` +
|
|
`script-src 'self'; ` +
|
|
`style-src 'self' 'unsafe-inline'; ` +
|
|
`img-src 'self' data:; ` +
|
|
`font-src 'self'; ` +
|
|
`connect-src 'self'; ` +
|
|
`frame-ancestors 'self'; ` +
|
|
`form-action 'self'; ` +
|
|
`base-uri 'self'; ` +
|
|
`object-src 'none';`;
|
|
|
|
res.setHeader('Content-Security-Policy', cspPolicy);
|
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
|
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
|
|
res.removeHeader('X-Powered-By');
|
|
|
|
// HSTS — only when explicitly configured to run over HTTPS.
|
|
if (process.env.HTTPS === 'true') {
|
|
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
|
}
|
|
|
|
next();
|
|
}
|
|
|
|
module.exports = { securityHeaders, getCspNonce };
|