Secure HTTP Header Configuration

HTTP response headers are the primary enforcement plane for browser security boundaries. A missing or misconfigured header does not merely degrade security posture — it opens a direct path for cross-site scripting (XSS) attacks, clickjacking, and protocol-downgrade exploits that bypass every server-side control downstream. This guide is part of Vulnerability Patterns & Web Mitigation Strategies. It maps each security header to its corresponding threat vector, walks through a production-ready implementation sequence, and provides the CI gate YAML and compliance tables needed to maintain the configuration under audit. Related controls — particularly CSRF defense and DOM-based vulnerability sanitization — operate in the same browser-execution layer and share implementation decisions with the patterns here.


Threat Anatomy

HTTP headers are advisory policies delivered in every response. The browser enforces them client-side; the server has no direct visibility into whether enforcement occurred. Attackers exploit this gap in three ways:

Missing header — no enforcement. Without Content-Security-Policy, a browser will happily execute injected scripts. MITRE ATT&CK T1059.007 (JavaScript for in-browser execution) and T1185 (browser session hijacking) both depend on the absence of declarative script restrictions.

Present but permissive header. A CSP with default-src * or script-src 'unsafe-inline' is functionally equivalent to no CSP. Attackers read the header from a preflight response and confirm that the whitelist is exploitable before injecting payload.

Header stripping or injection via intermediate proxies. Misconfigured load balancers, CDN edges, or reverse proxies may strip headers added upstream or inject conflicting ones. The browser applies only the last X-Frame-Options value it sees; duplicate values cause undefined behavior.

The attack surface spans five threat categories:

Threat Exploit mechanism Header control Attacker leverage without it
DOM-based XSS eval(), innerHTML, dynamic src attributes Content-Security-Policy (script-src, object-src) Full script execution in victim’s browser session
MIME sniffing Browser misinterprets uploaded file as executable X-Content-Type-Options: nosniff Drive-by malware execution from user-uploaded content
Clickjacking / UI redressing Malicious iframe overlays legitimate UI X-Frame-Options / CSP frame-ancestors Credential theft, unauthorized fund transfers
Referrer leakage Sensitive URL params exposed to third-party origins Referrer-Policy Token or session exfiltration via analytics or ad networks
Feature API abuse Compromised page accesses camera, geolocation, payment Permissions-Policy Privacy violation, covert data collection
Protocol downgrade First HTTP request intercepted before HSTS enforced Strict-Transport-Security + preload SSL stripping, credential interception on first visit

For the STRIDE perspective on which threats CSP and HSTS mitigate at each trust boundary, see the STRIDE framework implementation guidance, which classifies header weaknesses under Tampering and Information Disclosure.

The detailed guide Configuring HSTS and X-Frame-Options for Legacy Apps covers the specific rollout sequence for brownfield environments.


Prerequisites & Scope

Before applying these controls, confirm the following:

  • TLS is fully operational on all production origins, subdomains included. HSTS is counterproductive on HTTP-only endpoints and will cause browser errors if includeSubDomains is set before subdomain TLS is confirmed.
  • A CSP violation reporting endpoint exists. This can be a simple /csp-report route that logs to your SIEM, or a managed service. Without a report target, the report-only phase produces no data.
  • Your deployment pipeline can set response headers at the reverse proxy or CDN layer. Application-level header injection is acceptable but creates per-framework maintenance overhead; proxy-level headers apply uniformly across all routes.
  • Inline scripts and styles have been inventoried. CSP enforcement will break <script> tags without a nonce or hash. Identify all inline code — including third-party tag manager snippets — before enforcement.
  • You have baseline browser telemetry (browser version distribution). Certain CSP3 directives ('strict-dynamic', report-to) are not supported in every enterprise browser; telemetry determines whether graceful degradation fallbacks are needed.

Scope: these patterns apply to any HTTP application serving a browser. API-only backends that serve machine clients may skip X-Frame-Options and Permissions-Policy but must still apply Strict-Transport-Security, X-Content-Type-Options, and appropriate CORS headers.


Mitigation Architecture

The diagram below shows how headers layer across the request/response flow, from the browser through the CDN edge, reverse proxy, and application origin. Each boundary is a potential enforcement or bypass point.

HTTP Security Header Enforcement Architecture Diagram showing how HTTP security headers are enforced at each layer: browser, CDN edge, reverse proxy, and application origin. Untrusted zones are shown in red, boundary zones in yellow/orange, and trusted zones in green. Browser (untrusted zone) Parses CSP Enforces frame-ancestors Applies HSTS Enforces nosniff Referrer-Policy Permissions-Policy CSP violation report-to endpoint X-Frame-Options legacy fallback HTTPS CDN Edge (boundary zone) Can strip or duplicate headers ⚠ bypass risk HSTS injection at edge (preferred for performance) TLS termination; validate cert chain before HSTS active Cache: do NOT cache CSP nonce responses mTLS Reverse Proxy (boundary zone) add_header directives applied here for all upstream routes Rate-limit /csp-report to prevent report flooding Override or strip app-layer X-Powered-By Server info headers Path-based rules: stricter CSP on auth routes HTTP App Origin (trusted zone) Generates CSP nonce per response; injects into template Validates Origin / Referer server-side (CSRF layer) Sets Content-Type correctly (prevents MIME sniffing need) Processes CSP violation reports; forwards to SIEM Untrusted Boundary Trusted origin

The key architectural takeaway: headers set at the proxy layer apply uniformly to all routes including legacy endpoints, but CSP nonces must be generated at the application layer because they must change per-response. A CDN caching a nonce-bearing response will reuse a stale nonce that browsers will reject.


Step-by-Step Implementation

Step 1 — Inventory and Report-Only CSP Baseline (OWASP ASVS 14.4.1, NIST SC-18)

Before enforcing any CSP directive, capture what your application actually loads:

# nginx — report-only phase
add_header Content-Security-Policy-Report-Only
  "default-src 'self';
   script-src 'self';
   style-src 'self' 'unsafe-inline';
   img-src 'self' data: https:;
   font-src 'self';
   connect-src 'self';
   frame-ancestors 'none';
   report-uri /csp-report;"
always;

Forward reports to a searchable log store. In Express, a minimal report sink:

import express, { Request, Response } from 'express';

const router = express.Router();

// Rate-limit this endpoint — malicious actors can flood it to obscure real violations
router.post('/csp-report', express.json({ type: 'application/csp-report' }), (req: Request, res: Response) => {
  const report = req.body['csp-report'];
  if (report) {
    console.warn(JSON.stringify({
      level: 'csp_violation',
      blockedUri: report['blocked-uri'],
      violatedDirective: report['violated-directive'],
      documentUri: report['document-uri'],
      timestamp: new Date().toISOString(),
    }));
  }
  res.status(204).end();
});

export default router;

Run report-only for at least 7 days across representative traffic. Investigate every blocked-uri value — legitimate violations identify resources to whitelist; unknown or data-URI violations may indicate active probing.

Step 2 — Nonce Generation and Enforcement CSP (OWASP ASVS 14.4.3, NIST SC-18)

Replace unsafe-inline with per-request cryptographic nonces. The nonce changes on every response, so a cached copy cannot be reused by an attacker:

import crypto from 'crypto';
import helmet from 'helmet';
import express from 'express';

const app = express();

// Baseline headers via Helmet — CSP managed manually for nonce support
app.use(helmet({
  contentSecurityPolicy: false,
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
  xFrameOptions: { action: 'deny' },
  xContentTypeOptions: true,
  strictTransportSecurity: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true,
  },
}));

// Per-request nonce middleware — must run before template rendering
app.use((_req, res, next) => {
  const nonce = crypto.randomBytes(16).toString('base64');
  res.locals.cspNonce = nonce;

  res.setHeader(
    'Content-Security-Policy',
    [
      `default-src 'self'`,
      `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
      `style-src 'self' 'unsafe-inline'`,          // tighten with hashes after audit
      `img-src 'self' data: https:`,
      `font-src 'self'`,
      `connect-src 'self'`,
      `frame-ancestors 'none'`,
      `object-src 'none'`,
      `base-uri 'self'`,
      `form-action 'self'`,
      `upgrade-insecure-requests`,
      `report-uri /csp-report`,
    ].join('; ')
  );

  next();
});

// Expose nonce to templates (EJS / Handlebars / Nunjucks)
app.set('view engine', 'ejs');
app.get('/', (_req, res) => {
  res.render('index', { nonce: res.locals.cspNonce });
});

In your template:

<!-- EJS -->
<script nonce="<%= nonce %>">
  // inline bootstrap code here
</script>

'strict-dynamic' propagates nonce trust to scripts loaded by the nonced bootstrap, eliminating the need to whitelist every CDN origin individually. It is ignored by browsers that do not understand it, so add a 'nonce-{value}' fallback alongside it for the small percentage of legacy clients.

Step 3 — HSTS with Preload (OWASP ASVS 9.2.2, NIST SC-8)

HSTS protects returning visitors. The preload list eliminates the first-visit window where HSTS has not yet been seen:

# nginx — apply only after TLS is confirmed on ALL subdomains
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

Preload eligibility requires:

  • All subdomains serve HTTPS with valid certificates.
  • The preload token is present.
  • max-age is at least 31,536,000 seconds (one year).
  • The root domain redirects HTTP to HTTPS.

Verify compliance before submitting at hstspreload.org. Preloading is difficult to undo — confirm subdomain TLS coverage first.

Step 4 — Remaining Baseline Headers (OWASP ASVS 14.4.1–14.4.6)

Apply these at the proxy layer so they cover all routes uniformly:

# nginx — baseline header block
add_header X-Content-Type-Options      "nosniff"                            always;
add_header X-Frame-Options             "DENY"                               always;
add_header Referrer-Policy             "strict-origin-when-cross-origin"    always;
add_header Permissions-Policy          "camera=(), microphone=(), geolocation=(), payment=(self), usb=(), interest-cohort=()" always;

# Remove information-leaking headers
more_clear_headers Server;
more_clear_headers X-Powered-By;

Permissions-Policy now controls access to browser APIs such as the FLoC/Topics advertising APIs (interest-cohort=()). Audit it quarterly as new browser features appear in the W3C registry, since attackers probe newly standardized APIs before policies are updated to restrict them.


Edge Cases & Bypass Patterns

1. Whitelisted CDN origin with JSONP endpoint. CSP allows script-src https://cdn.example.com but that CDN also serves a JSONP callback endpoint. An attacker injects <script src="https://cdn.example.com/api?callback=alert"> — browsers execute it because the origin is trusted. Fix: never whitelist origins that serve JSONP. Migrate to CORS or subresource integrity hashes. This pattern is relevant to the attack surface mapping inventory step, which should flag JSONP endpoints as high-risk during assessment.

2. Open redirector on a whitelisted domain. If https://trusted.example.com/redirect?url=javascript:... is reachable and the domain is in script-src, some browser versions allow loading via the redirect chain. Enumerate open redirectors across whitelisted domains and remove or firewall them.

3. data: URI in default-src. Allowing data: in any source directive re-enables inline script injection through <script src="data:text/javascript,...">. Restrict to img-src at most, and never include data: in script-src or object-src.

4. frame-ancestors ignored by older browsers. Internet Explorer does not understand frame-ancestors. If your audience includes IE, maintain X-Frame-Options: DENY alongside frame-ancestors 'none' in CSP. The legacy app configuration guide covers the full dual-header migration sequence.

5. CDN response caching of nonce-bearing responses. A CDN edge that caches your HTML will serve the same nonce to multiple users. Subsequent users whose <script nonce> value differs from the cached header will have scripts blocked. Ensure authenticated or dynamic routes include Cache-Control: no-store or bypass edge caching entirely.


Automated Testing & CI Validation

Unit test — nonce uniqueness and CSP presence

import request from 'supertest';
import app from '../src/app';

describe('Security headers', () => {
  it('sets Content-Security-Policy on every response', async () => {
    const res = await request(app).get('/');
    expect(res.headers['content-security-policy']).toBeDefined();
  });

  it('generates a unique nonce per request', async () => {
    const [r1, r2] = await Promise.all([
      request(app).get('/'),
      request(app).get('/'),
    ]);
    const extractNonce = (csp: string) =>
      (csp.match(/'nonce-([^']+)'/) ?? [])[1];
    const n1 = extractNonce(r1.headers['content-security-policy']);
    const n2 = extractNonce(r2.headers['content-security-policy']);
    expect(n1).toBeDefined();
    expect(n2).toBeDefined();
    expect(n1).not.toBe(n2);
  });

  it('includes HSTS with preload directive', async () => {
    const res = await request(app).get('/');
    const hsts = res.headers['strict-transport-security'] ?? '';
    expect(hsts).toMatch(/max-age=\d+/);
    expect(hsts).toMatch(/includeSubDomains/);
    expect(hsts).toMatch(/preload/);
  });

  it('sets X-Content-Type-Options to nosniff', async () => {
    const res = await request(app).get('/');
    expect(res.headers['x-content-type-options']).toBe('nosniff');
  });
});

CI gate — GitHub Actions

name: Security Header Compliance
on: [pull_request, push]

jobs:
  header-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Start application
        run: docker compose up -d --build --wait
        timeout-minutes: 5

      - name: Fetch headers
        run: |
          curl -sk https://localhost:443 -o /dev/null -D headers.txt
          cat headers.txt

      - name: Assert mandatory headers present
        run: |
          grep -qi "strict-transport-security" headers.txt   || (echo "FAIL: HSTS missing"        && exit 1)
          grep -qi "x-content-type-options: nosniff" headers.txt || (echo "FAIL: nosniff missing" && exit 1)
          grep -qi "content-security-policy" headers.txt    || (echo "FAIL: CSP missing"         && exit 1)
          grep -qi "referrer-policy" headers.txt            || (echo "FAIL: Referrer-Policy"      && exit 1)
          grep -qi "permissions-policy" headers.txt         || (echo "FAIL: Permissions-Policy"   && exit 1)
          echo "All mandatory headers present"

      - name: Assert insecure headers absent
        run: |
          grep -qi "x-powered-by" headers.txt && (echo "FAIL: X-Powered-By leaks tech stack" && exit 1) || true
          grep -qi "x-xss-protection" headers.txt && (echo "WARN: X-XSS-Protection is deprecated, remove it") || true
          echo "Insecure header check passed"

      - name: Tear down
        if: always()
        run: docker compose down

Compliance Mapping

Framework Control ID Requirement Satisfied By
OWASP ASVS 4.0 14.4.1 HTTP response headers do not expose sensitive information Remove Server, X-Powered-By; Permissions-Policy
OWASP ASVS 4.0 14.4.3 CSP is in place and does not allow unsafe-eval or unsafe-inline script CSP with per-request nonce + object-src 'none'
OWASP ASVS 4.0 9.2.2 HSTS headers are present with max-age ≥ 31536000 Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
OWASP ASVS 4.0 14.4.6 X-Content-Type-Options: nosniff is set X-Content-Type-Options: nosniff
SOC 2 Type II CC6.1 Logical access controls prevent unauthorized access HSTS + CSP restrict execution to authorized origins
SOC 2 Type II CC7.2 System monitoring detects and responds to anomalies CSP violation reports forwarded to SIEM
NIST SP 800-53 SC-8 Transmission confidentiality and integrity HSTS eliminates plaintext channel; upgrade-insecure-requests in CSP
NIST SP 800-53 SC-18 Mobile code controls — restrict unauthorized mobile code CSP script-src, object-src 'none'
NIST SP 800-53 SC-23 Session authenticity X-Frame-Options / frame-ancestors prevents UI redressing; Referrer-Policy limits token leakage
PCI-DSS v4.0 6.4.2 Secure coding mitigates injection and client-side vulnerabilities CSP + nosniff directly address cardholder page injection
ISO 27001 A.14.1.3 Protecting application services on public networks HSTS, CSP, Referrer-Policy collectively protect public-facing endpoints

Common Pitfalls Checklist


Frequently Asked Questions

How do I safely migrate from X-Frame-Options to CSP frame-ancestors?

Run both headers concurrently during migration: X-Frame-Options: DENY and Content-Security-Policy: frame-ancestors 'none'. CSP takes precedence in browsers that understand it; X-Frame-Options provides fallback for Internet Explorer. Monitor your browser version distribution from analytics. Once IE is below 1% of your traffic and CSP violation reports show no legitimate framing, remove X-Frame-Options. Do not remove it without checking IE telemetry first.

What happens if HSTS max-age expires before a certificate renewal?

If max-age is set to a short value (e.g., 300 seconds for testing) and the TLS certificate lapses before the HSTS period ends, browsers will refuse to connect with a hard error the user cannot bypass. Always set a short max-age in staging, verify certificate auto-renewal is reliable, then increase max-age to 31536000 in production. Use certificate transparency monitoring to detect unexpected expiry or misissued certificates.

Which CSP directive is most commonly bypassed in practice?

Wildcard origins in script-src (e.g., https://cdn.example.com) combined with JSONP endpoints on the whitelisted domain are the most consistent real-world bypass. Attackers enumerate JSONP callback parameters on trusted CDNs. 'strict-dynamic' with nonces eliminates the need for domain whitelisting entirely and is the recommended upgrade path for modern browsers.

Does Referrer-Policy conflict with analytics platforms?

strict-origin-when-cross-origin sends the origin (scheme + host) but strips the path and query string for cross-origin requests. Most analytics platforms (server-side tagging, CDN log enrichment) only need the origin referrer to attribute traffic. Client-side analytics that depend on the full referrer URL for campaign attribution should migrate to first-party UTM parameters passed explicitly in the URL, which Referrer-Policy does not affect.