Security headers
Security headers, such as the ones Helmet sets, tell browsers to turn on protections against well-known web attacks like cross-site scripting and clickjacking. Examples are Content-Security-Policy, Strict-Transport-Security and X-Frame-Options.
Starting with NestJS v12.1, Nest can set these headers for you with app.useSecurityHeaders(). It uses the same defaults as Helmet 8, works the same way on Express and Fastify, and needs no extra package.
Getting started#
To enable the headers, call the useSecurityHeaders() method on the Nest application object:
// main.ts
const app = await NestFactory.create(AppModule);
app.useSecurityHeaders();
await app.listen(process.env.PORT ?? 3000);
The headers are set on every response: route responses, 404 responses, error responses, responses rejected by the CSRF protection, and Server-Sent Events streams. The X-Powered-By header is removed. The following rules apply:
- Call
useSecurityHeaders()beforeapp.init()orapp.listen(). Calling it afterwards, or calling it twice, throws an error. - Options are validated when you call the method, so a bad configuration fails at startup (see Configuring headers).
- The headers are written by a framework-level middleware (Express) or
onRequesthook (Fastify), registered at the point where you call the method. Middleware registered withapp.use()before that call runs first, and responses that such middleware sends itself don't carry the headers, so calluseSecurityHeaders()right afterNestFactory.create(). app.enableCsrfProtection()shares the same request hook. You can call the two methods in any order: the headers are always written before the CSRF check runs.
Default headers#
Without options, useSecurityHeaders() sends the following headers, with the same values as Helmet 8:
| Header | Default value | Option |
|---|---|---|
Content-Security-Policy | See Content Security Policy | contentSecurityPolicy |
Cross-Origin-Embedder-Policy | Not sent | crossOriginEmbedderPolicy |
Cross-Origin-Opener-Policy | same-origin | crossOriginOpenerPolicy |
Cross-Origin-Resource-Policy | same-origin | crossOriginResourcePolicy |
Origin-Agent-Cluster | ?1 | originAgentCluster |
Referrer-Policy | no-referrer | referrerPolicy |
Strict-Transport-Security | max-age=31536000; includeSubDomains | strictTransportSecurity |
X-Content-Type-Options | nosniff | xContentTypeOptions |
X-DNS-Prefetch-Control | off | xDnsPrefetchControl |
X-Download-Options | noopen | xDownloadOptions |
X-Frame-Options | SAMEORIGIN | xFrameOptions |
X-Permitted-Cross-Domain-Policies | none | xPermittedCrossDomainPolicies |
X-XSS-Protection | 0 | xXssProtection |
X-Powered-By | Removed | xPoweredBy |
Hint Browsers ignoreStrict-Transport-Securityover plain HTTP, andX-XSS-Protection: 0turns off the XSS auditor of old browsers, which was a source of vulnerabilities itself.
Configuring headers#
Each option controls one header. Pass false to leave the header out, true to send its default value, or an object to configure it:
app.useSecurityHeaders({
xFrameOptions: false,
strictTransportSecurity: { maxAge: 63072000, preload: true },
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
});
The options that accept an object take the following properties:
| Option | Properties |
|---|---|
contentSecurityPolicy | directives, useDefaults (default true) and reportOnly (default false). See Content Security Policy. |
crossOriginEmbedderPolicy | policy: 'require-corp' (sent for true), 'credentialless' or 'unsafe-none'. |
crossOriginOpenerPolicy | policy: 'same-origin' (default), 'same-origin-allow-popups', 'noopener-allow-popups' or 'unsafe-none'. |
crossOriginResourcePolicy | policy: 'same-origin' (default), 'same-site' or 'cross-origin'. |
referrerPolicy | policy: a referrer policy such as 'strict-origin-when-cross-origin' (default 'no-referrer'), or a list of policies, which browsers read as a fallback chain. |
strictTransportSecurity | maxAge in seconds (default 31536000, which is 365 days), includeSubDomains (default true) and preload (default false). |
xDnsPrefetchControl | allow (default false). true sends on. |
xFrameOptions | action: 'sameorigin' (default) or 'deny'. |
xPermittedCrossDomainPolicies | permittedPolicies: 'none' (default), 'master-only', 'by-content-type' or 'all'. |
The remaining options, originAgentCluster, xContentTypeOptions, xDownloadOptions, xPoweredBy and xXssProtection, accept only a boolean. For example, xPoweredBy: false keeps the X-Powered-By header.
Unknown options, unknown properties and invalid values throw when useSecurityHeaders() is called. Helmet's legacy option names are rejected with a hint to the current name, for example Invalid security headers configuration: unknown option "hsts" (helmet's legacy name, use "strictTransportSecurity").
A route can still override a header with the @Header() decorator, for example to let other sites load an image:
@Get('avatars/:id')
@Header('Cross-Origin-Resource-Policy', 'cross-origin')
findAvatar(@Param('id') id: string) {}
Hint On Fastify,reply.header()overrides a header as well, butreply.removeHeader()cannot remove a header set byuseSecurityHeaders().
Content Security Policy#
The default Content-Security-Policy is the same as Helmet 8's. It is sent on one line, without spaces after the semicolons:
default-src 'self';
base-uri 'self';
font-src 'self' https: data:;
form-action 'self';
frame-ancestors 'self';
img-src 'self' data:;
object-src 'none';
script-src 'self';
script-src-attr 'none';
style-src 'self' https: 'unsafe-inline';
upgrade-insecure-requests
Use the directives property to change it:
app.useSecurityHeaders({
contentSecurityPolicy: {
directives: {
scriptSrc: ["'self'", 'https://cdn.example.com'],
connectSrc: ["'self'", 'wss://realtime.example.com'],
upgradeInsecureRequests: null,
},
},
});
Directives work as follows:
- Names are written in camelCase (
scriptSrc) or in kebab-case ('script-src'). - A directive you set replaces the default directive of the same name, so include
'self'when you want to keep it. Other directives are added to the policy. - A value is a string or a list of strings.
trueor an empty list sends a directive without a value, andnullorfalseremoves a directive, including a default one. - Keywords, nonces and hashes must be single-quoted, as in
"'self'". An unquotedselfis rejected, because browsers would read it as a host name. - Values may only contain printable ASCII characters, and neither
;nor,, so a value cannot add directives or policies.
To send only your own directives, set useDefaults to false. The policy then needs a defaultSrc directive, unless you set defaultSrc: null to leave it out on purpose.
To try a policy without breaking pages, set reportOnly to true. Nest then sends Content-Security-Policy-Report-Only, and browsers report violations instead of blocking them. Add a report-to or report-uri directive to receive the reports.
app.useSecurityHeaders({
contentSecurityPolicy: { reportOnly: true },
});
To leave the header out entirely, set contentSecurityPolicy to false.
Swagger UI and GraphQL IDEs#
The policy applies to every response, including the HTML pages of API tools.
The Swagger UI served by @nestjs/swagger loads its scripts and stylesheets from your application, so it works with the default policy. If you add scripts with the customJs option, add their hosts to scriptSrc. Inline code from the customJsStr option requires 'unsafe-inline'. The default upgrade-insecure-requests directive makes browsers load scripts and stylesheets over HTTPS, so if you serve Swagger UI over plain HTTP outside local development, for example on a staging server reached by IP address, they fail to load. Serve Swagger UI over HTTPS, or remove the directive with upgradeInsecureRequests: null.
GraphiQL, as served by the Apollo driver, loads React and GraphiQL from unpkg.com and starts with an inline script. By default, GraphiQL is only enabled when NODE_ENV is not production, so relax the policy only there:
const isProduction = process.env.NODE_ENV === 'production';
app.useSecurityHeaders({
contentSecurityPolicy: isProduction
? true
: {
directives: {
scriptSrc: ["'self'", "'unsafe-inline'", 'https://unpkg.com'],
},
},
});
Apollo Sandbox, enabled with ApolloServerPluginLandingPageLocalDefault(), loads its scripts, images and manifest from Apollo's CDN, and embeds the Sandbox in a frame:
app.useSecurityHeaders({
contentSecurityPolicy: {
directives: {
scriptSrc: [
"'self'",
"'unsafe-inline'",
'https://embeddable-sandbox.cdn.apollographql.com',
],
imgSrc: [
"'self'",
'data:',
'https://apollo-server-landing-page.cdn.apollographql.com',
],
manifestSrc: [
"'self'",
'https://apollo-server-landing-page.cdn.apollographql.com',
],
frameSrc: ["'self'", 'https://sandbox.embed.apollographql.com'],
},
},
});
Warning'unsafe-inline'inscript-srcgives up most of the protection that CSP offers against cross-site scripting, for every page your application serves. Avoid it in production.
Using Helmet directly#
Applications that already use Helmet can keep using it. Since the option names of useSecurityHeaders() are the same as Helmet's, a Helmet configuration usually carries over when you switch, with two exceptions:
- CSP directive values must be strings, lists of strings, booleans or
null. Functions, which Helmet accepts to generate per-request nonces, are not supported. - Helmet's legacy option names are rejected:
| Legacy Helmet name | Option |
|---|---|
dnsPrefetchControl | xDnsPrefetchControl |
frameguard | xFrameOptions |
hidePoweredBy | xPoweredBy |
hsts | strictTransportSecurity |
ieNoOpen | xDownloadOptions |
noSniff | xContentTypeOptions |
permittedCrossDomainPolicies | xPermittedCrossDomainPolicies |
xssFilter | xXssProtection |
To use Helmet itself, install the package for your platform and register it before other calls to app.use() or setup functions that may call app.use(). The underlying platform applies middleware in the order it was registered, so Helmet registered after a route doesn't apply to that route. The CSP directives shown above work with Helmet's contentSecurityPolicy option as well.
Express
$ npm i helmet
// main.ts
import helmet from 'helmet';
app.use(helmet());
Fastify
With the FastifyAdapter, use the @fastify/helmet package. It is a Fastify plugin, not a middleware, so register it with app.register():
$ npm i @fastify/helmet
// main.ts
import helmet from '@fastify/helmet';
await app.register(helmet);

