Security Headers: CSP, HSTS & HTTP Hardening for My Portfolio
Implementing HSTS, Content Security Policy, and browser permission controls — what each header does and why it matters.
TL;DR Technical Overview: Implemented comprehensive security headers across three layers:
- HSTS with 2-year preload for transport security
- X-Frame-Options, X-Content-Type-Options, and Referrer-Policy for content protection
- Content Security Policy (CSP) and Permissions-Policy for attack surface reduction
Protection against XSS, clickjacking, MITM attacks, and MIME-sniffing vulnerabilities.
Security matters
Every website is a potential target. A simple portfolio can be used for phishing redirects, content injection, or as a link in a larger attack chain. Modern browsers support strong security mechanisms through HTTP headers, but they're opt-in. Nothing is enabled by default.
As a cybersecurity student, shipping a portfolio with obvious security gaps felt like a contradiction. So I implemented them.
Layer 1: Transport security
The foundation is ensuring all traffic uses HTTPS — but having an SSL certificate isn't enough. Browsers also need explicit instructions to never attempt insecure connections.
Strict-Transport-Security (HSTS) tells browsers to use HTTPS only for the next two years (max-age=63072000). includeSubDomains extends this to any future subdomains. preload gets the domain added to browsers' built-in HSTS lists, so protection kicks in before the very first response.
This blocks man-in-the-middle attacks that work by intercepting the initial HTTP request before it gets upgraded. Once a browser has seen this header, it won't attempt an insecure connection even if someone manually types http:// in the address bar.
Layer 2: Content protection
Three headers control how browsers handle the site's content.
X-Content-Type-Options: nosniff prevents browsers from guessing file types. Without it, a browser might interpret a JSON response as JavaScript and execute it. Strict MIME type adherence closes that vector.
X-Frame-Options: SAMEORIGIN blocks the site from being embedded in iframes on external domains. This prevents clickjacking — overlaying invisible iframes to get users to interact with elements they can't see.
Referrer-Policy: strict-origin-when-cross-origin limits what gets sent in the Referer header when users follow external links. Browsers send only the origin, not the full URL. Useful when URLs might contain query parameters you'd rather not expose to third-party domains.
Layer 3: Advanced restrictions
Content Security Policy is the most powerful of these headers, and the most work to configure correctly.
CSP defines explicit allowlists for every resource type. Scripts, styles, images, and fonts can only load from my own domain ('self'). I had to make compromises for Next.js compatibility — 'unsafe-inline' for styles and 'unsafe-eval' for scripts. These are documented trade-offs, not oversights. The img-src directive includes blob: and data: to support dynamically generated images and base64 assets that React depends on.
Permissions-Policy explicitly disables browser APIs the site doesn't use: camera, microphone, geolocation. Even if an XSS vulnerability slipped through, those APIs would be unavailable to an attacker. Attack surface reduction doesn't require perfection — just limiting what's exposed.
Testing
I tested headers using browser developer tools and a few online security scanners. During development, the browser console flagged CSP violations immediately, which made it straightforward to tighten the policy iteratively rather than guessing at what might be blocked in production.
$ npm audit
found 0 vulnerabilities
What I learned
Security headers are mostly configuration, not code. The mechanisms exist in browsers already — the work is understanding what each one does and what breaks when you enable it.
A misconfigured CSP can silently break features in hard-to-trace ways. Testing each directive in isolation before combining them works better than trying to get the full policy right in one pass.
Setting this up early also meant every feature I added afterward had to work within these constraints from the start. That's a lot easier than auditing and retrofitting later, and it reflects how security should work in production — built in, not bolted on.