Published on

Security Headers in Production — CSP, HSTS, and the Headers That Actually Matter

Authors

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 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.