Security Headers
Headers that protect without code changes
Security headers are HTTP response headers that tell the browser how to behave. They do not change your application logic. They instruct the browser to enforce security policies that reduce the impact of attacks.
Setting them takes one onResponse hook modification.
The headers
Strict-Transport-Security (HSTS)
Strict-Transport-Security: max-age=31536000; includeSubDomains Tells the browser: “For the next year, only access this site over HTTPS. Even if the user types http://, redirect to https:// automatically.”
Without this, a user’s first visit might be over HTTP (before the server redirects to HTTPS). An attacker on the network could intercept that first request. HSTS eliminates this window after the first visit.
X-Content-Type-Options
X-Content-Type-Options: nosniff Tells the browser: “Do not try to guess the content type. Trust the Content-Type header I send.”
Without this, the browser might “sniff” a file’s content type. An attacker could upload a file with a .txt extension that contains JavaScript. If the browser sniffs it as JavaScript and executes it, that is an XSS attack. nosniff prevents this.
X-Frame-Options
X-Frame-Options: DENY Tells the browser: “Do not allow this page to be embedded in an iframe.”
Without this, an attacker could embed your login page in an invisible iframe on their site and trick users into clicking buttons on your page (this is called clickjacking). DENY blocks all iframe embedding. Use SAMEORIGIN if you need iframes within your own site.
Content-Security-Policy (CSP)
Content-Security-Policy: default-src 'self' Tells the browser: “Only load resources (scripts, styles, images) from the same origin as this page.”
CSP is the most powerful defense against XSS. Even if an attacker injects a <script> tag into your page, the browser will not execute it because the script is not from your origin. CSP is a deep topic; default-src 'self' is the starting point.
[!NOTE] CSP can break your site if you use inline scripts, inline styles, or CDN-hosted resources. Start with
default-src 'self'and add exceptions as needed. A restrictive CSP that works is better than a permissive one that covers everything.
Referrer-Policy
Referrer-Policy: strict-origin-when-cross-origin Controls how much URL information is sent in the Referer header when navigating from your site to another site. strict-origin-when-cross-origin sends only the origin (not the full path) for cross-origin requests, and the full URL for same-origin requests. This prevents leaking sensitive URL paths (like reset tokens) in referrer headers.
Set them globally with onResponse
Update src/app.ts:
onResponse: ({ request, response, locals }) => {
const headers = new Headers(response.headers);
// Security headers
headers.set("x-content-type-options", "nosniff");
headers.set("x-frame-options", "DENY");
headers.set("referrer-policy", "strict-origin-when-cross-origin");
// Only set HSTS and CSP in production
if (process.env.NODE_ENV === "production") {
headers.set(
"strict-transport-security",
"max-age=31536000; includeSubDomains",
);
headers.set("content-security-policy", "default-src 'self'");
}
// Logging
const duration = Date.now() - locals.startTime;
const url = new URL(request.url);
log("request", {
method: request.method,
path: url.pathname,
status: response.status,
duration,
ip: locals.ip,
});
return new Response(response.body, {
status: response.status,
headers,
});
}, We skip HSTS in development because localhost uses HTTP. We skip CSP in development because it might block developer tools or hot reload scripts. In production, both should be set.
The other three headers (nosniff, DENY, strict-origin-when-cross-origin) are safe to set everywhere.
Exercises
Exercise 1: Add the security headers to your app. Make a request and inspect the response headers (use curl -v or your browser’s network tab). Verify all the headers are present.
Exercise 2: Try loading your app in an iframe from another page. With X-Frame-Options: DENY, the browser should refuse. Remove the header and try again to see the difference.
Exercise 3: Research the CSP header further. What would you need to add to default-src 'self' if your app loads Google Fonts? (Answer: style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com)
Why is X-Content-Type-Options: nosniff important for security?
Why do we skip HSTS in development?