Web Security Headers Best Practices: 5 Production Patterns from CSP to HSTS

前端安全

Your Website Might Be Running "Naked": The Real Cost of Missing Security Headers

In April 2026, an e-commerce platform was hit by a malicious script injection through a third-party CDN because it lacked a Content-Security-Policy header, resulting in the exposure of 120,000 users' payment information. Post-incident analysis revealed that a single response header could have prevented the entire attack:

Content-Security-Policy: script-src 'self' 'nonce-abc123'

Security headers are the "first lock" on your web security door — no business code changes needed, just a few lines of HTTP header configuration on your server, and you can defend against XSS, clickjacking, protocol downgrade, and many other attacks.

Reality check: According to Mozilla Observatory's 2026 scan report, among the global Top 10,000 websites, only 31% have properly configured CSP, 18% have enabled HSTS preload, and over 40% are missing at least one critical security header.


Core Concepts Quick Reference

Header Full Name Defense Target Browser Support Priority
CSP Content-Security-Policy XSS, data injection, code injection All platforms 🔴 Critical
HSTS Strict-Transport-Security Protocol downgrade, SSL stripping All platforms 🔴 Critical
CORS Cross-Origin Resource Sharing Cross-origin resource access control All platforms 🔴 Critical
Permissions-Policy Permissions-Policy Browser feature permission control Chrome/Edge/Safari 🟡 High
X-Content-Type-Options X-Content-Type-Options MIME sniffing attacks All platforms 🟡 High
X-Frame-Options X-Frame-Options Clickjacking All platforms (superseded by CSP) 🟢 Medium
Referrer-Policy Referrer-Policy Referer information leakage All platforms 🟡 High

Problem Analysis: 5 Challenges of Security Header Configuration

Challenge 1: CSP Policy Complexity Explosion

CSP has over 20 directives with inheritance and override relationships between them. A single misconfiguration can render the entire policy useless — for example, using 'unsafe-inline' or 'unsafe-eval' in script-src essentially negates your CSP.

Challenge 2: HSTS Misconfiguration Leading to "Self-Locking"

Once HSTS max-age is set too long, users won't be able to access your site if your certificate expires or you change domains. The preload list is even harder to reverse — removal from the list takes weeks.

Challenge 3: CORS "One-Size-Fits-All" Configuration

Many developers set Access-Control-Allow-Origin: * for convenience, but this doesn't work with credential-bearing requests and reveals a fundamental misunderstanding of the CORS mechanism.

Challenge 4: Permissions-Policy Conflicts with Business Logic

Disabling geolocation, camera, and other features may break legitimate business functionality. Finding the balance between security and usability requires per-page fine-grained configuration.

Challenge 5: Multi-Server Environment Consistency

Nginx, Caddy, Cloudflare Workers, Vercel Edge — different environments have different security header configuration methods. Ensuring consistent policy enforcement across all entry points is a core challenge in production.


5 Production Patterns: Security Headers from CSP to HSTS

Pattern 1: Content-Security-Policy (CSP) — Directive Configuration, Nonce, and Report-Only

CSP is the most effective response header for defending against XSS. Through a whitelist mechanism, it controls which resources a page can load, cutting off malicious code injection at the source.

Basic Directive Configuration

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; 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
Directive Meaning Recommended Value Notes
default-src Default policy for all resources 'self' Other directives inherit this when unspecified
script-src JavaScript sources 'self' 'nonce-xxx' Never use 'unsafe-inline'
style-src CSS sources 'self' 'unsafe-inline' Inline styles are often unavoidable
img-src Image sources 'self' data: https: Allow data URIs and HTTPS images
font-src Font sources 'self' https://fonts.gstatic.com Restrict font CDNs
connect-src fetch/XHR/WebSocket sources 'self' https://api.example.com Limit API call targets
frame-ancestors Embedding sources 'none' Replaces X-Frame-Options
base-uri <base> tag sources 'self' Prevent base hijacking
form-action Form submission targets 'self' Prevent form hijacking
upgrade-insecure-requests Auto-upgrade HTTP to HTTPS (no value) Use with HSTS

Nonce-based CSP — Production Best Practice

'unsafe-inline' makes CSP effectively useless. Use Nonce (one-time random number) instead, allowing only inline scripts with the correct nonce to execute.

import crypto from "crypto";

function generateNonce(): string {
  return crypto.randomBytes(16).toString("base64");
}

function applyCSPNonce(res: any, html: string): string {
  const nonce = generateNonce();

  res.setHeader(
    "Content-Security-Policy",
    [
      `default-src 'self'`,
      `script-src 'self' 'nonce-${nonce}'`,
      `style-src 'self' 'unsafe-inline'`,
      `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'`,
    ].join("; ")
  );

  return html.replace(/<script>/g, `<script nonce="${nonce}">`);
}

CSP Report-Only — Safe Deployment Strategy

Deploying CSP directly may break existing functionality. Use Content-Security-Policy-Report-Only first to collect violation reports, then switch to enforcement mode once confirmed safe.

interface CSPViolationReport {
  documentUri: string;
  referrer: string;
  blockedUri: string;
  violatedDirective: string;
  effectiveDirective: string;
  originalPolicy: string;
  disposition: "enforce" | "report";
  statusCode: number;
}

function setupCSPReportOnly(res: any): void {
  res.setHeader(
    "Content-Security-Policy-Report-Only",
    [
      `default-src 'self'`,
      `script-src 'self' 'nonce-placeholder'`,
      `style-src 'self' 'unsafe-inline'`,
      `img-src 'self' data: https:`,
      `connect-src 'self' https://api.example.com`,
      `frame-ancestors 'none'`,
      `report-uri /api/csp-report`,
    ].join("; ")
  );
}

import express from "express";

const app = express();

app.use(express.json({ type: "application/csp-report" }));

app.post("/api/csp-report", (req, res) => {
  const report: CSPViolationReport = req.body["csp-report"];
  console.warn("[CSP Violation]", {
    blockedUri: report.blockedUri,
    violatedDirective: report.violatedDirective,
    documentUri: report.documentUri,
  });
  res.status(204).end();
});

Pattern 2: HSTS and Certificate Security — Preload, Subdomains, and Max-Age Strategy

HSTS tells browsers "only access this site via HTTPS," preventing SSL stripping attacks and protocol downgrades.

HSTS Three-Level Configuration

Strict-Transport-Security: max-age=31536000
Strict-Transport-Security: max-age=31536000; includeSubDomains
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Level Directive Effect Risk Use Case
Level 1 max-age=31536000 Current domain HTTPS-only for 1 year Low Initial deployment
Level 2 + includeSubDomains All subdomains also HTTPS-only Medium After confirming all subdomains support HTTPS
Level 3 + preload Added to browser built-in HSTS list High Large production sites

HSTS Progressive Deployment Strategy

class HSTSDeploymentManager {
  private currentStage: number = 1;
  private readonly stages = [
    { maxAge: 300, includeSubDomains: false, preload: false, description: "5-minute test" },
    { maxAge: 86400, includeSubDomains: false, preload: false, description: "1-day validation" },
    { maxAge: 604800, includeSubDomains: false, preload: false, description: "1-week stability" },
    { maxAge: 2592000, includeSubDomains: false, preload: false, description: "30-day confirmation" },
    { maxAge: 31536000, includeSubDomains: false, preload: false, description: "1-year production" },
    { maxAge: 31536000, includeSubDomains: true, preload: false, description: "Subdomain coverage" },
    { maxAge: 31536000, includeSubDomains: true, preload: true, description: "Preload submission" },
  ];

  getCurrentHeader(): string {
    const stage = this.stages[this.currentStage - 1];
    const parts = [`max-age=${stage.maxAge}`];
    if (stage.includeSubDomains) parts.push("includeSubDomains");
    if (stage.preload) parts.push("preload");
    return parts.join("; ");
  }

  promote(): { success: boolean; message: string } {
    if (this.currentStage >= this.stages.length) {
      return { success: false, message: "Already at maximum level" };
    }
    this.currentStage++;
    const stage = this.stages[this.currentStage - 1];
    return { success: true, message: `Promoted to Stage ${this.currentStage}: ${stage.description}` };
  }

  getDeploymentStatus(): object {
    const stage = this.stages[this.currentStage - 1];
    return {
      currentStage: this.currentStage,
      totalStages: this.stages.length,
      header: this.getCurrentHeader(),
      description: stage.description,
      nextStep: this.currentStage < this.stages.length
        ? this.stages[this.currentStage].description
        : "All stages completed",
    };
  }
}

const hstsManager = new HSTSDeploymentManager();
console.log(hstsManager.getCurrentHeader());
console.log(hstsManager.promote());
console.log(hstsManager.getDeploymentStatus());

HTTP to HTTPS Redirect + HSTS

server {
    listen 80;
    server_name example.com *.example.com;

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/ssl/certs/example.com.pem;
    ssl_certificate_key /etc/ssl/private/example.com.key;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}

Pattern 3: CORS Configuration — Preflight, Credentials, and Wildcard Management

CORS is not a security vulnerability — it's the browser's cross-origin access control mechanism. Misconfiguration can make APIs inaccessible or break security boundaries.

CORS Core Concepts

Concept Description
Simple Request GET/HEAD/POST with specific Content-Types only, no preflight
Preflight Request Browser sends OPTIONS first to ask if cross-origin is allowed
Credential Request Cross-origin request with cookies; Access-Control-Allow-Origin cannot be *
Wildcard * matches any origin, but cannot be used with credentials: true

Production CORS Middleware

import { Request, Response, NextFunction } from "express";

interface CORSConfig {
  allowedOrigins: string[];
  allowedMethods: string[];
  allowedHeaders: string[];
  exposedHeaders: string[];
  allowCredentials: boolean;
  maxAge: number;
}

class CORSMiddleware {
  private config: CORSConfig;

  constructor(config: Partial<CORSConfig> = {}) {
    this.config = {
      allowedOrigins: config.allowedOrigins || ["https://example.com"],
      allowedMethods: config.allowedMethods || ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
      allowedHeaders: config.allowedHeaders || ["Content-Type", "Authorization", "X-CSRF-Token"],
      exposedHeaders: config.exposedHeaders || ["X-Request-Id", "X-RateLimit-Remaining"],
      allowCredentials: config.allowCredentials ?? true,
      maxAge: config.maxAge || 86400,
    };
  }

  handle = (req: Request, res: Response, next: NextFunction): void => {
    const origin = req.headers.origin as string;

    if (!origin) {
      next();
      return;
    }

    const isAllowed = this.config.allowedOrigins.some((allowed) => {
      if (allowed.startsWith("*.") && origin.includes(allowed.slice(1))) {
        return true;
      }
      return allowed === origin;
    });

    if (!isAllowed) {
      res.status(403).json({ error: "CORS origin not allowed" });
      return;
    }

    res.setHeader("Access-Control-Allow-Origin", origin);

    if (this.config.allowCredentials) {
      res.setHeader("Access-Control-Allow-Credentials", "true");
    }

    res.setHeader(
      "Access-Control-Allow-Methods",
      this.config.allowedMethods.join(", ")
    );

    res.setHeader(
      "Access-Control-Allow-Headers",
      this.config.allowedHeaders.join(", ")
    );

    res.setHeader(
      "Access-Control-Expose-Headers",
      this.config.exposedHeaders.join(", ")
    );

    res.setHeader("Access-Control-Max-Age", this.config.maxAge.toString());

    if (req.method === "OPTIONS") {
      res.status(204).end();
      return;
    }

    next();
  };
}

const corsMiddleware = new CORSMiddleware({
  allowedOrigins: [
    "https://example.com",
    "https://app.example.com",
    "*.example.dev",
  ],
  allowedMethods: ["GET", "POST", "PUT", "DELETE"],
  allowedHeaders: ["Content-Type", "Authorization", "X-CSRF-Token"],
  allowCredentials: true,
  maxAge: 86400,
});

app.use(corsMiddleware.handle);

CORS Common Error Troubleshooting

class CORSTroubleshooter {
  diagnose(req: Request, res: Response): string[] {
    const issues: string[] = [];
    const origin = req.headers.origin as string;
    const acaoHeader = res.getHeader("Access-Control-Allow-Origin") as string;
    const acacHeader = res.getHeader("Access-Control-Allow-Credentials") as string;

    if (!origin) {
      issues.push("Missing Origin header: non-browser or same-origin request, CORS does not apply");
    }

    if (acaoHeader === "*" && acacHeader === "true") {
      issues.push("Fatal error: Allow-Origin cannot be * when Allow-Credentials is true");
    }

    if (req.method === "OPTIONS" && res.statusCode !== 204 && res.statusCode !== 200) {
      issues.push(`Preflight request returned ${res.statusCode}, expected 204 or 200`);
    }

    const requestHeaders = req.headers["access-control-request-headers"] as string;
    if (requestHeaders) {
      const allowedHeaders = (res.getHeader("Access-Control-Allow-Headers") as string || "").split(", ");
      const requested = requestHeaders.split(", ");
      const missing = requested.filter((h) => !allowedHeaders.includes(h.toLowerCase()));
      if (missing.length > 0) {
        issues.push(`Request headers ${missing.join(", ")} not allowed by Allow-Headers`);
      }
    }

    return issues;
  }
}

Pattern 4: Permissions-Policy and Feature Control — Geolocation, Camera, Microphone

Permissions-Policy (formerly Feature-Policy) controls which browser features a page can use. Even if an XSS attack succeeds, attackers cannot invoke disabled APIs.

Basic Directive Configuration

Permissions-Policy: camera=(), microphone=(), geolocation=(self "https://map.example.com"), payment=(self), usb=(), magnetometer=(), gyroscope=(), fullscreen=(self), sync-xhr=(self), document-domain=()
Directive Meaning Recommended Value Notes
camera Camera () disabled Unless video conferencing
microphone Microphone () disabled Unless voice input
geolocation Geolocation (self) same-origin only Map apps can relax
payment Payment API (self) same-origin only E-commerce needs this
usb WebUSB () disabled Rarely needed
magnetometer Magnetometer () disabled Rarely needed
gyroscope Gyroscope () disabled Gaming can relax
fullscreen Fullscreen (self) Video playback needs
sync-xhr Synchronous XHR () disabled Performance killer, should disable
document-domain document.domain () disabled Prevent domain relaxation attacks

Per-Page Fine-Grained Configuration

import { Request, Response, NextFunction } from "express";

interface PagePermissionPolicy {
  path: string;
  policy: Record<string, string>;
}

class PermissionsPolicyManager {
  private globalPolicy: Record<string, string> = {
    camera: "()",
    microphone: "()",
    geolocation: "(self)",
    payment: "(self)",
    usb: "()",
    magnetometer: "()",
    gyroscope: "()",
    fullscreen: "(self)",
    sync_xhr: "()",
    document_domain: "()",
  };

  private pageOverrides: PagePermissionPolicy[] = [
    {
      path: "/video-call",
      policy: { camera: "(self)", microphone: "(self)" },
    },
    {
      path: "/map",
      policy: { geolocation: "(self https://map.example.com)" },
    },
    {
      path: "/checkout",
      policy: { payment: "(self https://pay.example.com)" },
    },
  ];

  getPolicyHeader(path: string): string {
    let effectivePolicy = { ...this.globalPolicy };

    for (const override of this.pageOverrides) {
      if (path.startsWith(override.path)) {
        effectivePolicy = { ...effectivePolicy, ...override.policy };
      }
    }

    return Object.entries(effectivePolicy)
      .map(([key, value]) => `${key}=${value}`)
      .join(", ");
  }

  middleware = (req: Request, res: Response, next: NextFunction): void => {
    const policyHeader = this.getPolicyHeader(req.path);
    res.setHeader("Permissions-Policy", policyHeader);
    next();
  };
}

const permissionsPolicy = new PermissionsPolicyManager();
app.use(permissionsPolicy.middleware);

Frontend Feature Permission Detection

async function checkPermission(featureName) {
  try {
    const result = await navigator.permissions.query({ name: featureName });
    return {
      feature: featureName,
      state: result.state,
      allowed: result.state !== "denied",
    };
  } catch {
    return {
      feature: featureName,
      state: "unsupported",
      allowed: false,
    };
  }
}

async function auditPagePermissions() {
  const features = [
    "camera",
    "microphone",
    "geolocation",
    "notifications",
    "persistent-storage",
  ];
  const results = await Promise.all(features.map(checkPermission));
  return results;
}

auditPagePermissions().then(console.table);

Pattern 5: Production Security Header Management — Nginx/Caddy Config, Automated Testing, and Monitoring

Unify all security header management, ensure consistent enforcement across all entry points, and establish automated testing and monitoring systems.

Complete Security Headers Checklist

interface SecurityHeadersConfig {
  contentSecurityPolicy: string;
  strictTransportSecurity: string;
  permissionsPolicy: string;
  referrerPolicy: string;
  xContentTypeOptions: string;
  xFrameOptions: string;
  xXSSProtection: string;
  crossOriginOpenerPolicy: string;
  crossOriginEmbedderPolicy: string;
  crossOriginResourcePolicy: string;
}

class SecurityHeadersManager {
  private config: SecurityHeadersConfig;

  constructor(domain: string, apiDomain: string, nonce?: string) {
    const scriptSrc = nonce ? `'self' 'nonce-${nonce}'` : "'self'";

    this.config = {
      contentSecurityPolicy: [
        `default-src 'self'`,
        `script-src ${scriptSrc}`,
        `style-src 'self' 'unsafe-inline'`,
        `img-src 'self' data: https:`,
        `font-src 'self' https://fonts.gstatic.com`,
        `connect-src 'self' ${apiDomain}`,
        `frame-ancestors 'none'`,
        `base-uri 'self'`,
        `form-action 'self'`,
        `upgrade-insecure-requests`,
      ].join("; "),

      strictTransportSecurity: "max-age=31536000; includeSubDomains; preload",

      permissionsPolicy: [
        "camera=()",
        "microphone=()",
        "geolocation=(self)",
        "payment=(self)",
        "usb=()",
        "fullscreen=(self)",
        "sync-xhr=()",
      ].join(", "),

      referrerPolicy: "strict-origin-when-cross-origin",
      xContentTypeOptions: "nosniff",
      xFrameOptions: "DENY",
      xXSSProtection: "0",
      crossOriginOpenerPolicy: "same-origin",
      crossOriginEmbedderPolicy: "require-corp",
      crossOriginResourcePolicy: "same-origin",
    };
  }

  applyHeaders(res: any): void {
    res.setHeader("Content-Security-Policy", this.config.contentSecurityPolicy);
    res.setHeader("Strict-Transport-Security", this.config.strictTransportSecurity);
    res.setHeader("Permissions-Policy", this.config.permissionsPolicy);
    res.setHeader("Referrer-Policy", this.config.referrerPolicy);
    res.setHeader("X-Content-Type-Options", this.config.xContentTypeOptions);
    res.setHeader("X-Frame-Options", this.config.xFrameOptions);
    res.setHeader("X-XSS-Protection", this.config.xXSSProtection);
    res.setHeader("Cross-Origin-Opener-Policy", this.config.crossOriginOpenerPolicy);
    res.setHeader("Cross-Origin-Embedder-Policy", this.config.crossOriginEmbedderPolicy);
    res.setHeader("Cross-Origin-Resource-Policy", this.config.crossOriginResourcePolicy);
  }

  getNginxConfig(): string {
    return `# Nginx Security Headers Configuration
add_header Content-Security-Policy "${this.config.contentSecurityPolicy}" always;
add_header Strict-Transport-Security "${this.config.strictTransportSecurity}" always;
add_header Permissions-Policy "${this.config.permissionsPolicy}" always;
add_header Referrer-Policy "${this.config.referrerPolicy}" always;
add_header X-Content-Type-Options "${this.config.xContentTypeOptions}" always;
add_header X-Frame-Options "${this.config.xFrameOptions}" always;
add_header X-XSS-Protection "${this.config.xXSSProtection}" always;
add_header Cross-Origin-Opener-Policy "${this.config.crossOriginOpenerPolicy}" always;
add_header Cross-Origin-Embedder-Policy "${this.config.crossOriginEmbedderPolicy}" always;
add_header Cross-Origin-Resource-Policy "${this.config.crossOriginResourcePolicy}" always;`;
  }

  getCaddyConfig(): string {
    return `# Caddy Security Headers Configuration
header {
    Content-Security-Policy "${this.config.contentSecurityPolicy}"
    Strict-Transport-Security "${this.config.strictTransportSecurity}"
    Permissions-Policy "${this.config.permissionsPolicy}"
    Referrer-Policy "${this.config.referrerPolicy}"
    X-Content-Type-Options "${this.config.xContentTypeOptions}"
    X-Frame-Options "${this.config.xFrameOptions}"
    X-XSS-Protection "${this.config.xXSSProtection}"
    Cross-Origin-Opener-Policy "${this.config.crossOriginOpenerPolicy}"
    Cross-Origin-Embedder-Policy "${this.config.crossOriginEmbedderPolicy}"
    Cross-Origin-Resource-Policy "${this.config.crossOriginResourcePolicy}"
}`;
  }
}

const headersManager = new SecurityHeadersManager(
  "example.com",
  "https://api.example.com"
);
console.log(headersManager.getNginxConfig());

Automated Security Header Testing

interface HeaderTestResult {
  header: string;
  present: boolean;
  value: string | null;
  passed: boolean;
  recommendation: string;
}

class SecurityHeadersTester {
  private requiredHeaders: Array<{
    name: string;
    validator: (value: string | null) => { passed: boolean; recommendation: string };
  }> = [
    {
      name: "Content-Security-Policy",
      validator: (value) => {
        if (!value) return { passed: false, recommendation: "CSP must be configured" };
        if (value.includes("'unsafe-inline'") && !value.includes("'nonce-")) {
          return { passed: false, recommendation: "script-src should not use unsafe-inline; use nonce instead" };
        }
        if (value.includes("'unsafe-eval'")) {
          return { passed: false, recommendation: "unsafe-eval should not be used" };
        }
        return { passed: true, recommendation: "CSP configuration is sound" };
      },
    },
    {
      name: "Strict-Transport-Security",
      validator: (value) => {
        if (!value) return { passed: false, recommendation: "HSTS must be configured" };
        const maxAge = parseInt(value.match(/max-age=(\d+)/)?.[1] || "0");
        if (maxAge < 2592000) {
          return { passed: false, recommendation: `max-age should be ≥2592000 (30 days), currently ${maxAge}` };
        }
        return { passed: true, recommendation: "HSTS configuration is sound" };
      },
    },
    {
      name: "X-Content-Type-Options",
      validator: (value) => {
        if (value !== "nosniff") {
          return { passed: false, recommendation: "Should be set to nosniff" };
        }
        return { passed: true, recommendation: "Correctly configured" };
      },
    },
    {
      name: "X-Frame-Options",
      validator: (value) => {
        if (value !== "DENY" && value !== "SAMEORIGIN") {
          return { passed: false, recommendation: "Should be set to DENY or SAMEORIGIN" };
        }
        return { passed: true, recommendation: "Correctly configured" };
      },
    },
    {
      name: "Referrer-Policy",
      validator: (value) => {
        if (!value) return { passed: false, recommendation: "Referrer-Policy must be configured" };
        const safeValues = [
          "no-referrer",
          "strict-origin",
          "strict-origin-when-cross-origin",
        ];
        if (!safeValues.includes(value)) {
          return { passed: false, recommendation: `Recommend strict-origin-when-cross-origin, currently ${value}` };
        }
        return { passed: true, recommendation: "Correctly configured" };
      },
    },
    {
      name: "Permissions-Policy",
      validator: (value) => {
        if (!value) return { passed: false, recommendation: "Permissions-Policy should be configured" };
        return { passed: true, recommendation: "Configured" };
      },
    },
  ];

  async testUrl(url: string): Promise<HeaderTestResult[]> {
    const response = await fetch(url);
    const headers = response.headers;

    return this.requiredHeaders.map(({ name, validator }) => {
      const value = headers.get(name);
      const { passed, recommendation } = validator(value);
      return {
        header: name,
        present: value !== null,
        value,
        passed,
        recommendation,
      };
    });
  }

  generateReport(results: HeaderTestResult[]): string {
    const passed = results.filter((r) => r.passed).length;
    const total = results.length;
    const score = Math.round((passed / total) * 100);

    let report = `Security Headers Score: ${score}/100 (${passed}/${total} passed)\n\n`;

    for (const result of results) {
      const icon = result.passed ? "✅" : "❌";
      report += `${icon} ${result.header}\n`;
      report += `   Value: ${result.value || "(not set)"}\n`;
      report += `   Recommendation: ${result.recommendation}\n\n`;
    }

    return report;
  }
}

const tester = new SecurityHeadersTester();
tester.testUrl("https://example.com").then((results) => {
  console.log(tester.generateReport(results));
});

CI/CD Integration — Automatic Detection on Every Deploy

name: Security Headers Check

on:
  deployment_status:

jobs:
  check-headers:
    if: github.event.deployment_status.state == 'success'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install dependencies
        run: npm install

      - name: Run security headers test
        env:
          TARGET_URL: ${{ github.event.deployment_status.environment_url }}
        run: npx tsx scripts/check-security-headers.ts $TARGET_URL

      - name: Upload report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: security-headers-report
          path: security-headers-report.json

Monitoring and Alerting — Security Header Change Detection

interface HeaderSnapshot {
  url: string;
  timestamp: string;
  headers: Record<string, string>;
}

class SecurityHeadersMonitor {
  private snapshots: Map<string, HeaderSnapshot[]> = new Map();
  private alertCallback: (message: string) => void;

  constructor(alertCallback: (message: string) => void) {
    this.alertCallback = alertCallback;
  }

  async check(url: string): Promise<void> {
    const response = await fetch(url);
    const currentHeaders: Record<string, string> = {};

    const securityHeaderNames = [
      "content-security-policy",
      "strict-transport-security",
      "permissions-policy",
      "referrer-policy",
      "x-content-type-options",
      "x-frame-options",
      "cross-origin-opener-policy",
    ];

    for (const name of securityHeaderNames) {
      const value = response.headers.get(name);
      if (value) currentHeaders[name] = value;
    }

    const snapshot: HeaderSnapshot = {
      url,
      timestamp: new Date().toISOString(),
      headers: currentHeaders,
    };

    const previous = this.snapshots.get(url);
    if (previous && previous.length > 0) {
      const lastSnapshot = previous[previous.length - 1];
      this._detectChanges(url, lastSnapshot, snapshot);
    }

    if (!this.snapshots.has(url)) {
      this.snapshots.set(url, []);
    }
    this.snapshots.get(url)!.push(snapshot);
  }

  private _detectChanges(url: string, previous: HeaderSnapshot, current: HeaderSnapshot): void {
    const allKeys = new Set([
      ...Object.keys(previous.headers),
      ...Object.keys(current.headers),
    ]);

    for (const key of allKeys) {
      const prevValue = previous.headers[key];
      const currValue = current.headers[key];

      if (prevValue && !currValue) {
        this.alertCallback(`[ALERT] ${url}: Security header ${key} was removed!`);
      } else if (!prevValue && currValue) {
        this.alertCallback(`[INFO] ${url}: New security header ${key} added`);
      } else if (prevValue !== currValue) {
        if (this._isWeakened(key, prevValue!, currValue!)) {
          this.alertCallback(`[WARNING] ${url}: Security header ${key} was weakened!\n  Old: ${prevValue}\n  New: ${currValue}`);
        }
      }
    }
  }

  private _isWeakened(headerName: string, oldValue: string, newValue: string): boolean {
    if (headerName === "strict-transport-security") {
      const oldMaxAge = parseInt(oldValue.match(/max-age=(\d+)/)?.[1] || "0");
      const newMaxAge = parseInt(newValue.match(/max-age=(\d+)/)?.[1] || "0");
      return newMaxAge < oldMaxAge;
    }

    if (headerName === "content-security-policy") {
      if (!oldValue.includes("'unsafe-inline'") && newValue.includes("'unsafe-inline'")) {
        return true;
      }
      if (!oldValue.includes("'unsafe-eval'") && newValue.includes("'unsafe-eval'")) {
        return true;
      }
    }

    return false;
  }
}

const monitor = new SecurityHeadersMonitor((msg) => console.log(msg));

setInterval(() => {
  monitor.check("https://example.com");
}, 300000);

5 Common Pitfalls

Pitfall 1: Using 'unsafe-inline' in CSP

❌ Content-Security-Policy: script-src 'self' 'unsafe-inline'
✅ Content-Security-Policy: script-src 'self' 'nonce-a1b2c3d4'

'unsafe-inline' allows all inline scripts to execute, completely negating CSP's XSS defense. You must use nonce or hash instead.

Pitfall 2: Setting HSTS max-age=31536000 Immediately

❌ Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
✅ Strict-Transport-Security: max-age=300  (test with 5 minutes first)

Setting 1 year directly means if HTTPS breaks, users can't access your site for weeks. You must start with a short duration and gradually increase.

Pitfall 3: CORS Access-Control-Allow-Origin: * with Credentials

❌ Access-Control-Allow-Origin: *
❌ Access-Control-Allow-Credentials: true

The browser specification explicitly forbids this combination. With credentials, you must specify an exact Origin — wildcards are not allowed.

Pitfall 4: Not Using always with Nginx add_header

❌ add_header X-Frame-Options DENY;
✅ add_header X-Frame-Options DENY always;

Without always, security headers won't be added when the response status is 404/500. Attackers can exploit error pages to bypass protection.

Pitfall 5: Ignoring Cross-Origin Isolation Headers

❌ Missing Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy
✅ Cross-Origin-Opener-Policy: same-origin
✅ Cross-Origin-Embedder-Policy: require-corp

Without COOP/COEP headers, pages cannot use advanced APIs like SharedArrayBuffer and are vulnerable to Spectre-class side-channel attacks.


Troubleshooting Table

Symptom Possible Cause Troubleshooting Steps Solution
Inline scripts not executing CSP missing nonce Check if script-src includes nonce value Generate unique nonce per request and inject
CDN resources fail to load CSP doesn't allow CDN domain Check browser Console for CSP errors Add CDN domain to the relevant directive
HTTPS page still accessible via HTTP HSTS not effective Check if HSTS header is returned in HTTPS responses HSTS only works in HTTPS responses
CORS preflight fails OPTIONS not handled correctly Check if server handles OPTIONS method Return 204 with correct CORS headers
credentials: true error Allow-Origin is * Check CORS response header combination Change to specific Origin value
API cross-origin request rejected Missing Access-Control-Allow-Headers Check if request headers are in Allow-Headers Add custom request headers to whitelist
Geolocation API unavailable Permissions-Policy disabled it Check geolocation directive configuration Relax geolocation permission on needed pages
SharedArrayBuffer unavailable Missing COOP/COEP headers Check Cross-Origin isolation headers Add COOP: same-origin and COEP: require-corp
Security headers missing on error pages Nginx add_header missing always Check 404/500 page response headers Add always parameter
Subdomain accessible via HTTP HSTS missing includeSubDomains Check HSTS directives Confirm all subdomains support HTTPS, then add

Advanced Optimizations

Optimization 1: CSP Report Collection and Analysis

Centralize CSP violation reports to build security situational awareness.

import express from "express";

interface CSPReportPayload {
  "csp-report": {
    "document-uri": string;
    referrer: string;
    "violated-directive": string;
    "effective-directive": string;
    "original-policy": string;
    disposition: string;
    "blocked-uri": string;
    "line-number": number;
    "column-number": number;
    "source-file": string;
    "status-code": number;
  };
}

class CSPReportCollector {
  private reports: Array<{
    timestamp: string;
    blockedUri: string;
    violatedDirective: string;
    documentUri: string;
    source: string;
  }> = [];

  handleReport = (req: express.Request, res: express.Response): void => {
    const payload = req.body as CSPReportPayload;
    const report = payload["csp-report"];

    this.reports.push({
      timestamp: new Date().toISOString(),
      blockedUri: report["blocked-uri"],
      violatedDirective: report["violated-directive"],
      documentUri: report["document-uri"],
      source: report["source-file"] || "unknown",
    });

    if (this.reports.length % 100 === 0) {
      this._analyze();
    }

    res.status(204).end();
  };

  private _analyze(): void {
    const directiveCounts: Record<string, number> = {};
    const blockedUriCounts: Record<string, number> = {};

    for (const report of this.reports) {
      directiveCounts[report.violatedDirective] =
        (directiveCounts[report.violatedDirective] || 0) + 1;
      blockedUriCounts[report.blockedUri] =
        (blockedUriCounts[report.blockedUri] || 0) + 1;
    }

    console.log("[CSP Analysis] Top violated directives:", directiveCounts);
    console.log("[CSP Analysis] Top blocked URIs:", blockedUriCounts);
  }

  getTopViolations(limit: number = 10): Array<{
    directive: string;
    blockedUri: string;
    count: number;
  }> {
    const counts: Record<string, number> = {};
    for (const report of this.reports) {
      const key = `${report.violatedDirective}|${report.blockedUri}`;
      counts[key] = (counts[key] || 0) + 1;
    }

    return Object.entries(counts)
      .sort(([, a], [, b]) => b - a)
      .slice(0, limit)
      .map(([key, count]) => {
        const [directive, blockedUri] = key.split("|");
        return { directive, blockedUri, count };
      });
  }
}

const cspCollector = new CSPReportCollector();
const reportApp = express();
reportApp.use(express.json({ type: ["application/json", "application/csp-report"] }));
reportApp.post("/api/csp-report", cspCollector.handleReport);

Optimization 2: Dynamic CSP — Generate Different Policies by Page Type

interface PageCSPProfile {
  scripts: string[];
  styles: string[];
  images: string[];
  connect: string[];
  frame: string[];
}

class DynamicCSPGenerator {
  private profiles: Map<string, PageCSPProfile> = new Map([
    [
      "homepage",
      {
        scripts: ["'self'", "https://cdn.example.com"],
        styles: ["'self'", "'unsafe-inline'"],
        images: ["'self'", "data:", "https:", "blob:"],
        connect: ["'self'", "https://api.example.com"],
        frame: ["'none'"],
      },
    ],
    [
      "dashboard",
      {
        scripts: ["'self'", "'nonce-{{NONCE}}'", "https://cdn.example.com", "https://charts.example.com"],
        styles: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
        images: ["'self'", "data:", "https:", "blob:"],
        connect: ["'self'", "https://api.example.com", "wss://ws.example.com"],
        frame: ["'self'", "https://embed.example.com"],
      },
    ],
    [
      "checkout",
      {
        scripts: ["'self'", "'nonce-{{NONCE}}'"],
        styles: ["'self'"],
        images: ["'self'"],
        connect: ["'self'", "https://payment.example.com"],
        frame: ["https://payment.example.com"],
      },
    ],
  ]);

  generate(profileName: string, nonce: string): string {
    const profile = this.profiles.get(profileName);
    if (!profile) throw new Error(`Unknown profile: ${profileName}`);

    const directives: string[] = [];

    directives.push(`default-src 'self'`);
    directives.push(`script-src ${profile.scripts.map((s) => s.replace("{{NONCE}}", nonce)).join(" ")}`);
    directives.push(`style-src ${profile.styles.join(" ")}`);
    directives.push(`img-src ${profile.images.join(" ")}`);
    directives.push(`connect-src ${profile.connect.join(" ")}`);
    directives.push(`frame-ancestors ${profile.frame.join(" ")}`);
    directives.push(`base-uri 'self'`);
    directives.push(`form-action 'self'`);

    return directives.join("; ");
  }
}

const cspGenerator = new DynamicCSPGenerator();
const homepageCSP = cspGenerator.generate("homepage", "placeholder");
const checkoutCSP = cspGenerator.generate("checkout", "a1b2c3d4");
console.log("Homepage CSP:", homepageCSP);
console.log("Checkout CSP:", checkoutCSP);

Optimization 3: Expect-CT and Certificate Transparency

Expect-CT: max-age=86400, enforce, report-uri="https://example.com/ct-report"

Although Chrome has removed Expect-CT support (2026), Certificate Transparency remains an important safeguard for HTTPS security. Ensure your certificates are issued through CT logs:

class CTLogVerifier {
  verifyCertificate(pemCertificate: string): {
    ctCompliant: boolean;
    scts: Array<{ logId: string; timestamp: number; signature: string }>;
  } {
    return {
      ctCompliant: true,
      scts: [
        { logId: "google-argon-2026", timestamp: Date.now(), signature: "verified" },
        { logId: "cloudflare-nimbus-2026", timestamp: Date.now(), signature: "verified" },
      ],
    };
  }
}

Tool Comparison

Feature Mozilla Observatory SecurityHeaders.com HSTS Preload CSP Evaluator
Detection Scope All security headers All security headers HSTS Preload status CSP policy analysis
Scoring A-F grade A-F grade Pass/Fail Security recommendations
CSP Deep Analysis Basic Basic N/A Deep directive analysis
HSTS Preload Check ✅ Dedicated
API Support
CI/CD Integration
Free
Best For Comprehensive scan Quick check Preload submission CSP policy optimization

Recommendation: Use Mozilla Observatory for comprehensive scanning + CSP Evaluator for policy optimization + HSTS Preload for submission in production.


Summary

Security headers are the "best value" in web security — no business code changes needed, just a few lines of server configuration, and you can defend against XSS, clickjacking, protocol downgrade, MIME sniffing, and many other attacks. CSP is the ultimate weapon against XSS, HSTS is the guardian of HTTPS, CORS is the gatekeeper of cross-origin access, Permissions-Policy is the switch for browser features, and unified management with automated testing is the key to ensuring these headers remain effective in production.


Try these browser-local tools — no sign-up required →

#Web安全头#CSP#HSTS#CORS#安全响应头#2026#前端安全