Secure Headers & CSP Builder
Add security headers safely without breaking functionality.
Essential Security Headers // middleware/security-headers.ts import { Request, Response, NextFunction } from "express";
export function securityHeaders( req: Request, res: Response, next: NextFunction ) { // Prevent clickjacking res.setHeader("X-Frame-Options", "DENY");
// Prevent MIME sniffing res.setHeader("X-Content-Type-Options", "nosniff");
// XSS Protection (legacy browsers) res.setHeader("X-XSS-Protection", "1; mode=block");
// Referrer Policy res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
// Permissions Policy (replaces Feature-Policy) res.setHeader( "Permissions-Policy", "camera=(), microphone=(), geolocation=(self), payment=()" );
// HSTS - Force HTTPS (only in production) if (process.env.NODE_ENV === "production") { res.setHeader( "Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload" ); }
next(); }
Content Security Policy (CSP) Phase 1: Report-Only Mode // config/csp-report-only.ts export const cspReportOnly = { "default-src": ["'self'"], "script-src": [ "'self'", "'report-sample'", "https://cdn.jsdelivr.net", "https://www.googletagmanager.com", ], "style-src": ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"], "img-src": ["'self'", "data:", "https:"], "font-src": ["'self'", "https://fonts.gstatic.com"], "connect-src": ["'self'", "https://api.example.com"], "frame-ancestors": ["'none'"], "base-uri": ["'self'"], "form-action": ["'self'"], "report-uri": ["/api/csp-report"], };
function formatCSP(policy: Record${key} ${values.join(" ")})
.join("; ");
}
// Apply report-only header app.use((req, res, next) => { res.setHeader( "Content-Security-Policy-Report-Only", formatCSP(cspReportOnly) ); next(); });
CSP Violation Reporter // routes/csp-report.ts app.post( "/api/csp-report", express.json({ type: "application/csp-report" }), (req, res) => { const violation = req.body["csp-report"];
console.error("CSP Violation:", {
documentUri: violation["document-uri"],
violatedDirective: violation["violated-directive"],
blockedUri: violation["blocked-uri"],
sourceFile: violation["source-file"],
lineNumber: violation["line-number"],
});
// Store in monitoring system
trackCSPViolation({
directive: violation["violated-directive"],
blockedUri: violation["blocked-uri"],
userAgent: req.headers["user-agent"],
timestamp: new Date(),
});
res.status(204).send();
} );
Phase 2: Enforce Mode // config/csp-enforce.ts export const cspEnforce = { "default-src": ["'self'"], "script-src": [ "'self'", // Add nonces for inline scripts "'nonce-{NONCE}'", "https://cdn.jsdelivr.net", "https://www.googletagmanager.com", ], "style-src": [ "'self'", // Replace unsafe-inline with nonces "'nonce-{NONCE}'", "https://fonts.googleapis.com", ], "img-src": ["'self'", "data:", "https:"], "font-src": ["'self'", "https://fonts.gstatic.com"], "connect-src": ["'self'", "https://api.example.com"], "frame-ancestors": ["'none'"], "base-uri": ["'self'"], "form-action": ["'self'"], "upgrade-insecure-requests": [], };
// Generate nonce for each request app.use((req, res, next) => { const nonce = crypto.randomBytes(16).toString("base64"); res.locals.cspNonce = nonce;
const policy = formatCSP(cspEnforce).replace(/{NONCE}/g, nonce);
res.setHeader("Content-Security-Policy", policy); next(); });
Nonce Implementation // views/index.ejs
Secure Page
Helmet.js Integration // Using Helmet for comprehensive security headers import helmet from "helmet";
app.use( helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'nonce-{NONCE}'"], styleSrc: ["'self'", "'nonce-{NONCE}'"], imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'", "https://api.example.com"], fontSrc: ["'self'", "https://fonts.gstatic.com"], objectSrc: ["'none'"], mediaSrc: ["'self'"], frameSrc: ["'none'"], }, }, hsts: { maxAge: 31536000, includeSubDomains: true, preload: true, }, frameguard: { action: "deny", }, xssFilter: true, noSniff: true, referrerPolicy: { policy: "strict-origin-when-cross-origin", }, }) );
Rollout Plan
CSP Rollout Plan
Week 1: Report-Only Mode
- [ ] Deploy CSP in report-only mode
- [ ] Monitor violation reports
- [ ] Identify problematic resources
- [ ] Whitelist legitimate sources
Week 2: Analysis
- [ ] Analyze 1 week of violations
- [ ] Update CSP policy based on reports
- [ ] Fix inline scripts/styles
- [ ] Test on staging
Week 3: Staged Rollout
- [ ] Enable enforcement for 10% of traffic
- [ ] Monitor error rates
- [ ] Check user reports
- [ ] Adjust policy if needed
Week 4: Full Enforcement
- [ ] Enable for 50% of traffic
- [ ] Verify no issues
- [ ] Enable for 100% of traffic
- [ ] Keep report-only header for monitoring
Testing CSP // tests/csp.test.ts import { describe, it, expect } from "vitest"; import request from "supertest"; import { app } from "../src/app";
describe("Content Security Policy", () => { it("should set CSP header", async () => { const response = await request(app).get("/");
expect(response.headers["content-security-policy"]).toBeDefined();
expect(response.headers["content-security-policy"]).toContain(
"default-src 'self'"
);
});
it("should block inline scripts without nonce", async () => {
const html = <!DOCTYPE html>
<html>
<head>
<script>alert('blocked')</script>
</head>
</html>;
// This would be blocked by CSP
// Verify in browser console or automated tests
});
it("should allow scripts with valid nonce", async () => { const response = await request(app).get("/");
// Extract nonce from response
const nonceMatch = response.text.match(/nonce="([^"]+)"/);
expect(nonceMatch).toBeDefined();
}); });
Common CSP Issues & Fixes // Issue 1: Inline event handlers // ❌ Bad
// ✅ Good
// Issue 2: Inline styles // ❌ Bad
// ✅ Good
// Issue 3: eval() usage // ❌ Bad eval('console.log("test")');
// ✅ Good // Don't use eval - refactor code
// Issue 4: Third-party scripts // ❌ Bad - no CSP entry
// ✅ Good - whitelisted in CSP script-src: ['self', 'https://cdn.example.com']
Monitoring & Alerts // monitoring/csp-violations.ts import { CloudWatch } from "@aws-sdk/client-cloudwatch";
const cloudwatch = new CloudWatch();
export async function trackCSPViolation(violation: { directive: string; blockedUri: string; userAgent: string; timestamp: Date; }) { await cloudwatch.putMetricData({ Namespace: "Security/CSP", MetricData: [ { MetricName: "Violations", Value: 1, Unit: "Count", Timestamp: violation.timestamp, Dimensions: [ { Name: "Directive", Value: violation.directive, }, { Name: "BlockedUri", Value: violation.blockedUri, }, ], }, ], });
// Alert if violations spike
if (await isViolationSpike()) {
await sendAlert({
title: "CSP Violation Spike Detected",
message: High number of violations for ${violation.directive},
});
}
}
Best Practices Start report-only: Don't break production Gradual rollout: 10% → 50% → 100% Use nonces: Better than unsafe-inline Monitor violations: Track and analyze Test thoroughly: All pages and features Document exceptions: Why resources whitelisted Regular audits: Quarterly CSP review Output Checklist Security headers implemented CSP policy defined (report-only) CSP violation reporter endpoint Nonce generation for inline scripts Helmet.js configured Rollout plan documented Testing strategy implemented Monitoring and alerts configured Team trained on CSP Staged rollout completed