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
includeSubDomainsis set before subdomain TLS is confirmed. - A CSP violation reporting endpoint exists. This can be a simple
/csp-reportroute 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.
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
preloadtoken is present. max-ageis 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.
Related
- Cross-Site Scripting (XSS) Mitigation — CSP is a mitigation layer that complements input sanitization and output encoding at the XSS prevention layer
- DOM-Based Vulnerability Sanitization —
object-src 'none'andbase-uri 'self'in CSP directly constrain DOM manipulation vectors - Cross-Site Request Forgery (CSRF) Defense —
Referrer-PolicyandSameSitecookie attributes share an implementation layer and interact with header configuration - Configuring HSTS and X-Frame-Options for Legacy Apps — step-by-step rollout for brownfield environments where full TLS subdomain coverage is incremental
- Vulnerability Patterns & Web Mitigation Strategies — parent section covering the full spectrum of web attack classes and their mitigations