HelmetSecurity · NestJS

A dozen response headers that tell browsers what not to do with your responses, set once by helmet on every response the API sends, with one directive adjusted for the shelter's image CDN.

What you will learn

Read the theory for Helmet

All Security lessons

All NestJS courses

loading types…

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 says http://. Only meaningful once the API is served over HTTPS.
  • X-Content-Type-Options: nosniff: take the Content-Type as 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 of frame-ancestors, for browsers without CSP.
  • Referrer-Policy: no-referrer: do not send this page's URL to the next site.
  • Cross-Origin-Opener-Policy and Cross-Origin-Resource-Policy, both same-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: ?1 and X-XSS-Protection: 0 (yes, off: the browser filter it controlled caused more harm than it prevented). helmet also removes X-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.

  1. In main.ts, register helmet before anything else, with the content security policy's img-src allowing 'self', data: and cdn.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 after app.listen() and never ran for a request.
  • The policy names only img-src: the whole contentSecurityPolicy object was replaced with useDefaults: false, or the directives were passed at the wrong level; the defaults merge only under directives.
  • 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 helmet instead 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 nosniff and 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.