security-headers.ts 1.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
  1. export interface SecurityHeadersConfig {
  2. enableHsts: boolean;
  3. cspMode: "report-only" | "enforce" | "disabled";
  4. cspReportUri?: string;
  5. hstsMaxAge: number;
  6. frameOptions: "DENY" | "SAMEORIGIN";
  7. }
  8. export const DEFAULT_SECURITY_HEADERS_CONFIG: SecurityHeadersConfig = {
  9. enableHsts: false,
  10. cspMode: "report-only",
  11. hstsMaxAge: 31536000,
  12. frameOptions: "DENY",
  13. };
  14. function isValidCspReportUri(uri: string): boolean {
  15. const trimmed = uri.trim();
  16. if (!trimmed || trimmed.includes(";") || trimmed.includes(",") || /\s/.test(trimmed)) {
  17. return false;
  18. }
  19. try {
  20. new URL(trimmed);
  21. return true;
  22. } catch {
  23. return false;
  24. }
  25. }
  26. const DEFAULT_CSP_VALUE =
  27. "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' " +
  28. "'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self'; font-src 'self' data:; " +
  29. "frame-ancestors 'none'";
  30. export function buildSecurityHeaders(
  31. config?: Partial<SecurityHeadersConfig>
  32. ): Record<string, string> {
  33. const merged = { ...DEFAULT_SECURITY_HEADERS_CONFIG, ...config };
  34. const headers: Record<string, string> = {};
  35. headers["X-Content-Type-Options"] = "nosniff";
  36. headers["X-Frame-Options"] = merged.frameOptions;
  37. headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
  38. headers["X-DNS-Prefetch-Control"] = "off";
  39. if (merged.enableHsts) {
  40. headers["Strict-Transport-Security"] = `max-age=${merged.hstsMaxAge}; includeSubDomains`;
  41. }
  42. if (merged.cspMode !== "disabled") {
  43. const headerName =
  44. merged.cspMode === "report-only"
  45. ? "Content-Security-Policy-Report-Only"
  46. : "Content-Security-Policy";
  47. if (merged.cspReportUri && isValidCspReportUri(merged.cspReportUri)) {
  48. headers[headerName] = `${DEFAULT_CSP_VALUE}; report-uri ${merged.cspReportUri}`;
  49. } else {
  50. headers[headerName] = DEFAULT_CSP_VALUE;
  51. }
  52. }
  53. return headers;
  54. }