The platform infrastructure layer — config and runtime that protects every user before your app code ever runs.
TLS does exactly three things: confidentiality (encrypt in transit), integrity (detect tampering), and authenticity (prove you're talking to the claimed server via a certificate). All three are required — encrypting the wrong server is useless.
Once a browser sees Strict-Transport-Security: max-age=63072000; includeSubDomains; preload it never sends http:// for that origin — even if the user types it. The first visit is still vulnerable (TOFU: trust on first use). The preload list solves this: browsers ship with a hardcoded list of HSTS domains so even the first request is HTTPS. Submit via hstspreload.org — preload is irreversible short-term, you need includeSubDomains, and all your subdomains must be HTTPS-ready first.
Active mixed content (scripts, iframes, stylesheets over HTTP on an HTTPS page) is always blocked — the network context would be compromised. Passive mixed content (images, media) was historically shown with a warning but Chrome now auto-upgrades or blocks it. Fix: use protocol-relative URLs (//cdn.example.com/...) or HTTPS everywhere. The upgrade-insecure-requests CSP directive automatically upgrades passive HTTP requests.
TLS proves you're on an encrypted channel to the right server — it can't stop your own XSS or server bugs. Set HSTS with long max-age and preload once your entire subdomain estate is HTTPS-ready. That's a platform decision, not a per-page one.
These go in your nginx/CDN config (or middleware), not in every HTML file. L19 covered Content-Security-Policy and frame-ancestors. The remaining headers complete the hardening set.
| Header | What it does | Recommended value |
|---|---|---|
X-Content-Type-Options |
Stops browsers from MIME sniffing — executing a response as a different type than its Content-Type (e.g., serving JS as text/plain but browser runs it) |
nosniff |
Referrer-Policy |
Controls what URL is sent in the Referer header on navigation/requests. Sensitive: booking URLs, auth tokens in query strings |
strict-origin-when-cross-origin (full URL same-origin, only origin cross-origin) |
Permissions-Policy |
Disables browser APIs for the page and its embedded iframes. Defense against malicious third-party scripts requesting geolocation/camera | geolocation=(), camera=(), microphone=() — disable what you don't need |
Strict-Transport-Security |
HSTS (§1 above) | max-age=63072000; includeSubDomains |
X-Frame-Options |
Legacy clickjacking defense (L19). Superseded by CSP frame-ancestors |
SAMEORIGIN (for browsers that don't support CSP) |
Security headers are infra config — set them in your CDN or server and they apply to every response. A Lead governs these at the platform level rather than trusting each team to add them per-route. Run securityheaders.com on your domain to audit coverage.
Permissions-Policy: interest-cohort=() was commonly used to opt out of FLoC (Google's ad targeting API, now deprecated). Its replacement — the Privacy Sandbox Topics API — requires its own opt-out. These headers need to be revisited as browser APIs evolve, not set once and forgotten.
A Service Worker is a JavaScript proxy running in a background thread between your page and the network. It can intercept every fetch request, serve from a local cache, or even respond when offline. It does not run on the main thread — no DOM access — and it persists across page loads.
// sw.js — install: cache the app shell self.addEventListener('install', event => { event.waitUntil( caches.open('v1').then(cache => cache.addAll(['/', '/app.js', '/styles.css']) ) ); self.skipWaiting(); // take over immediately — see pitfall below }); // activate: clean up old caches self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(keys => Promise.all(keys.filter(k => k !== 'v1').map(k => caches.delete(k))) ) ); self.clients.claim(); // take control of existing pages immediately });
// fetch: cache-first for JS/CSS, network-first for API self.addEventListener('fetch', event => { const { request } = event; if (request.url.includes('/api/')) { // network-first for API event.respondWith( fetch(request) .then(res => { /* cache and return */ return res; }) .catch(() => caches.match(request)) ); } else { // cache-first for static assets event.respondWith( caches.match(request).then(cached => cached || fetch(request)) ); } });
A new SW version (even a 1-byte change) enters "waiting" state — it won't activate until all tabs using the old SW are closed. This means users on long-lived sessions never get the update.
Common "fix" and its trap: calling skipWaiting() in install forces the new SW to activate immediately. But now the new SW is serving requests for a page that was loaded with the old SW's assets. If the new and old versions of assets are incompatible, the page breaks silently.
postMessage to the page.A Service Worker is a client-side CDN you control — cache-first for app shell, network-first for live data. The hidden trap is the update lifecycle: a new SW waits for all tabs to close. Ship content-hashed assets so old and new coexist safely, then give users a reload prompt.
Subresource Integrity lets you pin a specific external file to a cryptographic hash. The browser computes the hash of what it fetches and refuses to execute it if it doesn't match.
<!-- If the CDN is compromised and the file changes, browser blocks it --> <script src="https://cdn.example.com/react.production.min.js" integrity="sha384-Znh5+iBGuvijOqLzOG+Lzxjbii7bJIU6MkHMoJ3B5j+Qra/A2SmMaSCWzxLQKA==" crossorigin="anonymous"></script>
crossorigin="anonymous" is required — SRI performs a CORS fetch, so the CDN must send CORS headers and you must set the attribute.
If an attacker modifies a file on the CDN (or poisons the cache), the hash mismatch blocks it. Protects third-party libraries loaded from external CDNs.
Dependencies bundled into your own JS are already inside your code — SRI can't help. You need lockfiles + audit for those.
Your node_modules tree is a supply chain attack surface. Real incidents: event-stream (2018, injected crypto mining), colors/faker (2022, maintainer sabotage), ua-parser-js (2021, account hijack). Attack vectors:
lodahs vs lodash; attacker publishes a malicious package with a similar name.| Layer | Defense |
|---|---|
| Lockfile | Commit package-lock.json/yarn.lock. Exact versions + hashes. npm ci (not npm install) in CI — fails on lockfile mismatch. |
| Audit | npm audit + Dependabot/Snyk in CI — gate merges on high-severity CVEs. |
| Private registry | Proxy npm through Artifactory/Verdaccio. Allowlist — only pre-approved packages pass through. Fixes dependency confusion (private packages resolve via your registry first). |
| SBOM | Software Bill of Materials — generate a manifest of every dependency + version for compliance and incident response. |
| External scripts | SRI + crossorigin for CDN-loaded libs. Self-host critical third-party scripts when possible (breaks SRI auto-update, so weigh the trade-off). |
| CSP | script-src allowlist limits where scripts can load from (L19). Combined with SRI: belt + braces. |
SRI pins a specific external file to a hash — great for CDN-loaded libs. Your bundled npm tree is a different surface: lockfiles + npm ci + Dependabot + private registry. A Lead treats the supply chain as an attack surface and builds automated gates, not manual reviews.
Headers you should be able to recite for the round, grouped by threat:
| Threat | Header(s) | Lesson |
|---|---|---|
| XSS execution | Content-Security-Policy (script-src, nonce, strict-dynamic) | L19 |
| Clickjacking | Content-Security-Policy: frame-ancestors, X-Frame-Options | L19 |
| MIME sniffing | X-Content-Type-Options: nosniff | L20 |
| Protocol downgrade | Strict-Transport-Security | L20 |
| Referrer leakage | Referrer-Policy: strict-origin-when-cross-origin | L20 |
| API abuse / fingerprinting | Permissions-Policy | L20 |
| CDN compromise | integrity attribute (SRI) | L20 |
0 / 8 correct
"Walk me through every HTTP response header you'd set on a high-traffic HTTPS booking page to harden it against the most common attacks. Explain what each one stops."
Target: cover CSP (script-src + nonce + frame-ancestors), HSTS, nosniff, Referrer-Policy, Permissions-Policy, X-Frame-Options fallback. Should take ~2 minutes. No notes.
Good follow-up topics: