Skip to main content
ADR-017accepted

Defense-in-Depth HTTP Security Headers

Context

HTTP security headers are the first layer of defense against common web attacks: XSS (Cross-Site Scripting), clickjacking, MIME-type sniffing, and information leakage. Next.js provides no security headers by default — the developer must explicitly configure them in next.config.js. A portfolio site has a smaller attack surface than an authenticated application, but it's still publicly accessible and serves as a professional representation. A security vulnerability on a CTO's portfolio site would be particularly ironic and reputationally damaging. The security headers must balance protection against real-world attack vectors with compatibility across browsers and embedded contexts (social media previews, link unfurling bots, search engine rendering).

Decision

Configure seven security headers applied to all routes (/:path*) via next.config.js async headers. X-DNS-Prefetch-Control: on — enables DNS prefetching for faster navigation to linked domains. Strict-Transport-Security: max-age=63072000; includeSubDomains; preload — enforces HTTPS for 2 years across all subdomains, with HSTS preload eligibility. X-Content-Type-Options: nosniff — prevents MIME-type sniffing attacks where browsers reinterpret response content types. X-Frame-Options: SAMEORIGIN — prevents clickjacking by disallowing iframe embedding from other origins while allowing same-origin embedding. X-XSS-Protection: 1; mode=block — enables legacy XSS filter in older browsers (modern browsers use CSP instead, but this provides backward compatibility). Referrer-Policy: strict-origin-when-cross-origin — sends full referrer for same-origin requests, origin-only for cross-origin requests, protecting URL paths from leaking to external sites. Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=() — explicitly disables unnecessary browser APIs and opts out of FLoC/Topics API tracking.

Consequences

Positive: SecurityHeaders.com score of A+. HSTS preload eligibility protects against SSL stripping attacks and ensures all requests use HTTPS after the first visit. nosniff prevents a class of attacks where malicious content disguises its MIME type. SAMEORIGIN frame protection prevents the site from being embedded in phishing pages while allowing legitimate same-origin embedding. The Permissions-Policy explicitly opts out of surveillance capitalism APIs (FLoC/Topics) — a principled stance for a privacy-aware technologist. The configuration is applied globally via next.config.js, so new pages automatically inherit all headers. Negative: X-Frame-Options: SAMEORIGIN prevents the site from being embedded in third-party tools that might legitimately want to preview it (e.g., link unfurling services that render in iframes). However, most modern unfurling services use server-side rendering, so this is rarely an issue. The HSTS preload submission is irreversible at the browser level — once a domain is on the preload list, removing HTTPS support would break the site for all users with updated preload lists. The X-XSS-Protection header is technically deprecated in modern browsers and can cause false positives in edge cases, but the mode=block directive minimizes this risk. No Content-Security-Policy (CSP) header is configured — this is a deliberate omission because styled-components generates inline styles that would require 'unsafe-inline' in the CSP, negating much of its XSS protection value.

Calibrated Uncertainty

Predictions at Decision Time

Expected a SecurityHeaders.com score of A or higher. Predicted the headers would not break any legitimate functionality (social media previews, search engine rendering, browser compatibility). Assumed the missing CSP header would not be a significant security gap for a static portfolio without user-generated content. Predicted the HSTS preload commitment would be acceptable given no plans to ever revert to HTTP.

Measured Outcomes

SecurityHeaders.com score is A+ as targeted. No legitimate functionality has been broken — social media previews (Twitter, LinkedIn, Slack) work correctly because they use server-side rendering for unfurling, not iframe embedding. Search engines render the site correctly despite the headers. The missing CSP has not been exploited (no XSS vectors exist on a static site without user input), confirming the prediction. HSTS preload commitment is comfortable — there is zero scenario where the site would revert to HTTP. The unexpected observation: the Permissions-Policy header disabling interest-cohort=() is now partially redundant because Google deprecated FLoC in favor of Topics API, but the header still serves as a documented privacy stance.

Unknowns at Decision Time

Did not know at decision time whether CSP nonce-based approach would become compatible with styled-components' runtime injection model. As of early 2026, styled-components v6 has experimental CSP nonce support, but the portfolio uses v5.3 and migration would require significant effort. Also unknown: whether X-XSS-Protection would be removed from browser implementations entirely, potentially causing unexpected behavior. Chrome has already removed it, but the header is harmless when unsupported. Unknown: whether future portfolio features (comment system, contact form with file upload) would require relaxing any of these headers.

Reversibility Classification

Two-Way Door

Each header is independently removable by editing the securityHeaders array in next.config.js. Adding, modifying, or removing individual headers requires a single line change plus redeploy. The only exception is HSTS preload: once submitted to the preload list, removal requires a formal delisting process that takes months. However, the HSTS header itself can be removed immediately — the preload list effect persists independently in browser distributions. Estimated effort: 5 minutes for any header change.

Strongest Counter-Argument

For a static portfolio site with zero user input and no authentication, the security header configuration is over-engineered. The actual attack surface is minimal: there are no forms to exploit (except a contact form that submits to a third-party), no cookies to steal, no sessions to hijack. The headers protect against theoretical attacks on a site with no practical attack vectors. A simpler approach: rely on Vercel's default headers and Next.js's built-in protections. The counter-counter: security headers are cheap insurance — the configuration took 30 minutes to implement and adds zero runtime cost. More importantly, for a CTO's portfolio, demonstrating security awareness through headers is a professional signal, not just a technical measure.

Technical Context

Stack
next.config.jsHTTP HeadersHSTSPermissions-Policy
Security Headers Score
A+
Headers Configured
7
Hsts Max Age
63072000 seconds (2 years)
Disabled Browser Apis
4
Constraints
  • No CSP due to styled-components inline styles
  • HSTS preload is effectively irreversible
  • X-Frame-Options may block legitimate embedding

Related Decisions