- Published on
Security Headers in Production — CSP, HSTS, and the Headers That Actually Matter
- Authors

- Name
- Sanjeev Sharma
- @webcoderspeed1
Introduction
Security headers are the lowest-hanging fruit in defense-in-depth. They stop XSS, clickjacking, MIME-type sniffing, and credential leakage with a few response headers. Yet most APIs skip them, treating them as frontend concerns or assuming CDNs handle them.
This post covers the headers that actually matter: Content-Security-Policy with nonces, HSTS with preload, X-Frame-Options, Permissions-Policy, and cross-origin policies (COEP, COOP, CORP). All with production code.
- Content-Security-Policy with Nonces
- CSP Report-Only Mode for Gradual Rollout
- HSTS with Preload List
- X-Frame-Options and Frame-Ancestors
- Permissions-Policy for Browser Features
- Cross-Origin Policies: COEP, COOP, CORP
- Using Helmet.js for Easy Configuration
- Testing Headers with securityheaders.com
- Header Configuration by Environment
- Checklist
- Conclusion
Content-Security-Policy with Nonces
CSP blocks inline scripts and external script injection:
import express from 'express';
import { randomBytes } from 'crypto';
interface CSPOptions {
useNonce: boolean;
reportOnly: boolean;
reportUri?: string;
}
class ContentSecurityPolicyMiddleware {
private options: CSPOptions;
constructor(options: CSPOptions = { useNonce: true, reportOnly: true }) {
this.options = options;
}
middleware() {
return (
req: express.Request,
res: express.Response,
next: express.NextFunction
) => {
const nonce = randomBytes(16).toString('base64');
res.locals.nonce = nonce;
// Build CSP directives
const directives: Record<string, string[]> = {
'default-src': ["'self'"],
'script-src': [
"'self'",
`'nonce-${nonce}'`, // Inline scripts with matching nonce
'https://cdn.example.com', // Trusted CDN
],
'style-src': [
"'self'",
`'nonce-${nonce}'`, // Inline styles with nonce
'https://fonts.googleapis.com',
],
'img-src': ["'self'", 'data:', 'https:'],
'font-src': ["'self'", 'https://fonts.gstatic.com'],
'connect-src': [
"'self'",
'https://api.example.com',
'https://analytics.example.com',
],
'frame-ancestors': ["'self'"],
'form-action': ["'self'"],
'base-uri': ["'self'"],
};
if (this.options.reportUri) {
directives['report-uri'] = [this.options.reportUri];
}
const cspHeader = Object.entries(directives)
.map(([key, values]) => `${key} ${values.join(' ')}`)
.join('; ');
const headerName = this.options.reportOnly
? 'Content-Security-Policy-Report-Only'
: 'Content-Security-Policy';
res.setHeader(headerName, cspHeader);
next();
};
}
}
const app = express();
const cspMiddleware = new ContentSecurityPolicyMiddleware({
useNonce: true,
reportOnly: false, // Enforcing after testing
reportUri: 'https://report.example.com/csp',
});
app.use(cspMiddleware.middleware());
// Use nonce in templates
app.get('/', (req, res) => {
const nonce = res.locals.nonce;
res.send(`
<!DOCTYPE html>
<html>
<head>
<script nonce="${nonce}">
console.log('This inline script is allowed');
</script>
</head>
<body>
<h1>Secure Page</h1>
</body>
</html>
`);
});
CSP Report-Only Mode for Gradual Rollout
Test CSP before enforcing:
interface CSPReportData {
'document-uri': string;
'violated-directive': string;
'effective-directive': string;
'original-policy': string;
'blocked-uri': string;
'source-file': string;
'line-number': number;
'column-number': number;
'disposition': 'enforce' | 'report';
}
class CSPReporter {
async handleCSPReport(
req: express.Request,
res: express.Response
): Promise<void> {
const report = req.body as CSPReportData;
console.warn('CSP Violation:', {
uri: report['document-uri'],
directive: report['violated-directive'],
blockedUri: report['blocked-uri'],
source: report['source-file'],
});
// Store in database for analysis
await db.cspReports.create({
documentUri: report['document-uri'],
violatedDirective: report['violated-directive'],
blockedUri: report['blocked-uri'],
sourceFile: report['source-file'],
timestamp: new Date(),
});
// Alert on critical violations
if (
['script-src', 'style-src'].includes(
report['violated-directive']
)
) {
await alertSecurityTeam('CSP violation detected', report);
}
res.status(204).send();
}
}
const reporter = new CSPReporter();
app.post(
'/api/csp-report',
express.json({ type: 'application/csp-report' }),
(req, res) => reporter.handleCSPReport(req, res)
);
// Gradual rollout strategy
// 1. Deploy with report-only mode
// 2. Collect reports for 2 weeks
// 3. Identify exceptions and add to allowlist
// 4. Switch to enforcing mode
HSTS with Preload List
Force HTTPS across all subdomains:
interface HSTSConfig {
maxAge: number; // seconds
includeSubDomains: boolean;
preload: boolean;
}
class HSTSMiddleware {
private config: HSTSConfig;
constructor(config: HSTSConfig = {
maxAge: 63072000, // 2 years
includeSubDomains: true,
preload: true,
}) {
this.config = config;
}
middleware() {
return (
req: express.Request,
res: express.Response,
next: express.NextFunction
) => {
let hstsValue = `max-age=${this.config.maxAge}`;
if (this.config.includeSubDomains) {
hstsValue += '; includeSubDomains';
}
if (this.config.preload) {
hstsValue += '; preload';
}
res.setHeader('Strict-Transport-Security', hstsValue);
next();
};
}
}
const app = express();
// Enforce HTTPS redirect first
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
return res.redirect(
`https://${req.header('host')}${req.url}`
);
}
next();
});
// Then apply HSTS
app.use(
new HSTSMiddleware({
maxAge: 63072000, // 2 years
includeSubDomains: true,
preload: true,
}).middleware()
);
// Register on https://hstspreload.org/
// This adds example.com to Chrome/Firefox/Safari preload lists
// HSTS applies even on first visit
X-Frame-Options and Frame-Ancestors
Prevent clickjacking:
type FrameOptions = 'DENY' | 'SAMEORIGIN' | 'ALLOW-FROM';
interface ClickjackingConfig {
mode: FrameOptions;
allowedFrameOrigins?: string[];
}
class ClickjackingProtection {
private config: ClickjackingConfig;
constructor(
config: ClickjackingConfig = {
mode: 'SAMEORIGIN',
}
) {
this.config = config;
}
middleware() {
return (
req: express.Request,
res: express.Response,
next: express.NextFunction
) => {
// X-Frame-Options (legacy, but still useful)
let frameOptions = this.config.mode;
if (
this.config.mode === 'ALLOW-FROM' &&
this.config.allowedFrameOrigins
) {
frameOptions = `ALLOW-FROM ${this.config.allowedFrameOrigins[0]}`;
}
res.setHeader('X-Frame-Options', frameOptions);
// Content-Security-Policy frame-ancestors (modern, preferred)
const frameSources =
this.config.mode === 'DENY'
? "'none'"
: this.config.mode === 'SAMEORIGIN'
? "'self'"
: this.config.allowedFrameOrigins?.join(' ') || "'none'";
const currentCSP = res.getHeader(
'Content-Security-Policy'
) as string;
const newCSP = `${currentCSP}; frame-ancestors ${frameSources}`;
res.setHeader('Content-Security-Policy', newCSP);
next();
};
}
}
const app = express();
app.use(
new ClickjackingProtection({
mode: 'SAMEORIGIN',
}).middleware()
);
// Admin endpoints: Stricter
app.use('/admin', (req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY');
next();
});
Permissions-Policy for Browser Features
Control which features JavaScript can access:
class PermissionsPolicyMiddleware {
middleware() {
return (
req: express.Request,
res: express.Response,
next: express.NextFunction
) => {
// Disable sensitive features
const permissionsPolicy = [
'accelerometer=()',
'ambient-light-sensor=()',
'autoplay=()',
'camera=()',
'display-capture=()',
'document-domain=()',
'encrypted-media=()',
'fullscreen=(self)',
'geolocation=()',
'gyroscope=()',
'magnetometer=()',
'microphone=()',
'midi=()',
'payment=()',
'picture-in-picture=()',
'sync-xhr=(self "https://api.example.com")',
'usb=()',
'xr-spatial-tracking=()',
].join(', ');
res.setHeader('Permissions-Policy', permissionsPolicy);
next();
};
}
}
const app = express();
app.use(new PermissionsPolicyMiddleware().middleware());
Cross-Origin Policies: COEP, COOP, CORP
Protect against Spectre-like attacks:
class CrossOriginPolicies {
middleware() {
return (
req: express.Request,
res: express.Response,
next: express.NextFunction
) => {
// Cross-Origin-Opener-Policy: Isolate window from cross-origin openers
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
// Cross-Origin-Embedder-Policy: Require CORS for all subresources
// (Enables high-precision timers for mitigating Spectre)
res.setHeader(
'Cross-Origin-Embedder-Policy',
'require-corp'
);
// Cross-Origin-Resource-Policy: Control who can embed this resource
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
// X-Content-Type-Options: Prevent MIME sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// X-XSS-Protection (legacy, but still useful for older browsers)
res.setHeader('X-XSS-Protection', '1; mode=block');
// Referrer-Policy: Control referrer information
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
next();
};
}
}
const app = express();
app.use(new CrossOriginPolicies().middleware());
// For subresources (images, scripts, etc.), apply CORP
app.use('/static', (req, res, next) => {
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
next();
});
app.use(express.static('public'));
Using Helmet.js for Easy Configuration
Helmet provides sensible defaults:
import helmet from 'helmet';
const app = express();
// Apply all sensible defaults
app.use(helmet());
// Custom configuration
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
"'self'",
'https://cdn.example.com',
],
styleSrc: ["'self'", 'https://fonts.googleapis.com'],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'", 'https://api.example.com'],
fontSrc: ["'self'", 'https://fonts.gstatic.com'],
formAction: ["'self'"],
frameAncestors: ["'self'"],
},
reportUri: 'https://report.example.com/csp',
},
hsts: {
maxAge: 63072000, // 2 years
includeSubDomains: true,
preload: true,
},
frameguard: {
action: 'sameorigin',
},
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
xssFilter: true,
})
);
// Nonce injection for CSP
app.use((req, res, next) => {
res.locals.nonce = randomBytes(16).toString('base64');
next();
});
Testing Headers with securityheaders.com
# Verify all headers are set correctly
curl -I https://api.example.com
# Expected output includes:
# Content-Security-Policy: ...
# Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
# X-Content-Type-Options: nosniff
# X-Frame-Options: SAMEORIGIN
# X-XSS-Protection: 1; mode=block
# Referrer-Policy: strict-origin-when-cross-origin
# Cross-Origin-Opener-Policy: same-origin
# Cross-Origin-Embedder-Policy: require-corp
# Permissions-Policy: ...
# Submit to https://securityheaders.com for grading
Header Configuration by Environment
const getSecurityHeaders = (env: string) => {
const baseConfig = {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", 'https://fonts.googleapis.com'],
},
},
};
if (env === 'production') {
return {
...baseConfig,
hsts: {
maxAge: 63072000,
includeSubDomains: true,
preload: true,
},
contentSecurityPolicy: {
...baseConfig.contentSecurityPolicy,
reportUri: 'https://report.example.com/csp',
},
};
}
if (env === 'staging') {
return {
...baseConfig,
contentSecurityPolicy: {
...baseConfig.contentSecurityPolicy,
reportOnly: true,
},
};
}
// Development: More permissive
return {
contentSecurityPolicy: false,
};
};
app.use(
helmet(getSecurityHeaders(process.env.NODE_ENV || 'development'))
);
Checklist
- Deploy Content-Security-Policy with nonces for inline scripts
- Use report-only mode for 2 weeks before enforcing
- Enable HSTS with includeSubDomains and preload
- Register domain on hstspreload.org
- Set X-Frame-Options to SAMEORIGIN or DENY
- Deploy Permissions-Policy to restrict browser features
- Use COEP and COOP for Spectre mitigation
- Set X-Content-Type-Options: nosniff to prevent MIME sniffing
- Verify all headers with curl and securityheaders.com
- Audit CSP reports weekly and update allowlist
Conclusion
Security headers are force multipliers—they stop entire classes of attacks (XSS, clickjacking, MIME sniffing) with zero application logic. Deploy them with helmet.js, start in report-only mode, and enforce after testing. Combined with CSP nonces and HSTS preloading, your production API becomes far more resistant to client-side and transport-layer attacks.