Files
What you'll learn
- Explain what security response headers do: instructions to the browser, enforced by the browser, that cost the server nothing
- Register helmet as the first global middleware so every response, including errors and 404s, carries its headers
- Read the headers helmet sets by default and name the ones that matter for a JSON API
- Adjust one content security policy directive without losing the others, and turn a header off when it must be
Helmet
The cats API answers JSON, but the thing reading it is usually a browser, and a browser does a lot on its own initiative: it guesses at content types, lets a page be framed inside another site's page, loads scripts from wherever a page says, sends the full page URL along as a referrer, and downgrades to plain HTTP when asked. Each of those has been the first step of an attack. The defence is a set of response headers, small instructions the server attaches to every response and the browser enforces. They cost the server nothing, and they are easy to get wrong one by one, which is why helmet exists: one middleware that sets a sensible dozen of them.
What the headers say
Run with helmet and read a response in the request panel. The headers are the lesson:
Content-Security-Policy: where a page may load scripts, styles, images, fonts and frames from. helmet's default is'self'for almost everything, which blocks inline scripts and third-party sources;frame-ancestors 'self'also stops other sites from embedding yours. For a JSON API this header protects the rare HTML response (an error page, a docs page) and does nothing for JSON, and it is the one you will adjust most.Strict-Transport-Security:max-age=31536000; includeSubDomains, telling the browser to use HTTPS for this host for a year, even when a link sayshttp://. Only meaningful once the API is served over HTTPS.X-Content-Type-Options: nosniff: take theContent-Typeas given, never guess. Without it a browser may execute a JSON response as script if it looks like one. This is the header that matters most for an API.X-Frame-Options: SAMEORIGIN: the older form offrame-ancestors, for browsers without CSP.Referrer-Policy: no-referrer: do not send this page's URL to the next site.Cross-Origin-Opener-PolicyandCross-Origin-Resource-Policy, bothsame-origin: isolate the page from windows it opened and refuse to be loaded as a resource by another origin, which CORS (lesson 7) then opens deliberately.X-DNS-Prefetch-Control: off,X-Download-Options: noopen,X-Permitted-Cross-Domain-Policies: none,Origin-Agent-Cluster: ?1andX-XSS-Protection: 0(yes, off: the browser filter it controlled caused more harm than it prevented). helmet also removesX-Powered-By, which Express adds and which tells an attacker what to try.
Registering it, and where
helmet is Express middleware, registered on the app once:
import helmet from 'helmet';
app.use(helmet());
The docs' one rule is order: register helmet before any other app.use() and before anything that answers requests. Express runs middleware in registration order, and a response sent before helmet ran has no headers. In a Nest app the routes are bound at listen, so app.use(helmet()) anywhere between create and listen precedes them, but a static-file handler or another app.use() placed above it does not get the headers.
Because it is middleware, it runs before routing. That means a 404 for a route that does not exist, a 401 from a guard, a 500 from a thrown error, all carry the headers too. That is the test of a global middleware: an error response is still a response.
Adjusting a directive
Defaults are a starting point. A site that serves images from a CDN needs img-src to name it; helmet takes the directive by its camel-cased name and merges it into the defaults, so the other directives stay:
app.use(
helmet({
contentSecurityPolicy: {
directives: {
scriptSrc: ["'self'", 'scripts.example.com'],
},
},
}),
);
'self' is quoted twice on purpose, once for JavaScript and once for the header, where it is a CSP keyword; a bare self would be a hostname. A header can also be turned off, contentSecurityPolicy: false, which the docs do for an API that hosts a GraphQL playground the policy would break. Turn one off when it breaks something you need, not in advance.
Your task
The shelter's photos come from cdn.shelter.example.
- In
main.ts, register helmet before anything else, with the content security policy'simg-srcallowing'self',data:andcdn.shelter.example, and every other directive at its default.
Then send a request to a path that does not exist and read its headers: the 404 is Nest's, the headers are helmet's.
When it fails
- No headers at all:
app.use(helmet())is missing, or was written afterapp.listen()and never ran for a request. - The policy names only
img-src: the wholecontentSecurityPolicyobject was replaced withuseDefaults: false, or the directives were passed at the wrong level; the defaults merge only underdirectives. img-src self data:without quotes in the header: the JavaScript string was'self'where the header needs"'self'".TypeError: helmet is not a function:import * as helmetinstead of the default import; helmet is a default export.
Remember
- Security headers are instructions the browser enforces; helmet sets a dozen sensible ones with one
app.use(helmet()). - Register it first; middleware runs in order, and an error response is a response too.
- For a JSON API
nosniffand HSTS carry the weight; CSP guards the odd HTML page and is the one you tune. - Adjust a directive under
contentSecurityPolicy.directives, in camel case, and the rest stay at their defaults.
Stuck? Show a hint
main.ts: import helmet from 'helmet'; app.use(helmet({ contentSecurityPolicy: { directives: { imgSrc: ["'self'", 'data:', 'cdn.shelter.example'] } } })) right after NestFactory.create, before listen.
Press Run tests to start the app. Its log appears here.Graded endpoints
The body is the usual JSON; the headers are helmet's, including the content security policy with the shelter's image sources merged into the defaults
Middleware runs before routing, so a response Nest generates for a route that does not exist carries the same headers
The rest of the API is unchanged; every response carries the headers
Guarded as in lesson 1
A 401 the guard produced, with the headers too
Staff add a cat
Public
Even a 204 with no body carries the headers