Securing server-rendered apps
If you are creating a server-rendered application using AdonisJS, then you must use the @adonisjs/shield
package to protect your applications from common web attacks like CSRF, XSS, Content sniffing, and so on.
The package comes pre-configured with the web starter kit. However, you can manually install the package as follows.
The @adonisjs/shield
package has a peer dependency on the @adonisjs/session
package, so make sure to configure the session package first.
npm i @adonisjs/shield
yarn add @adonisjs/shield
pnpm add @adonisjs/shield
Once done, you must run the following command to configure the shield package.
node ace configure @adonisjs/shield
-
Registers the following service provider inside the
adonisrc.ts
file.{providers: [// ...other providers() => import('@adonisjs/shield/shield_provider'),]} -
Creates the
config/shield.ts
file. -
Registers the following middleware inside the
start/kernel.ts
file.router.use([() => import('@adonisjs/shield/shield_middleware')])
CSRF protection
CSRF (Cross-Site Request Forgery) is an attack in which a malicious website tricks the users of your web app to perform form submissions without their explicit consent.
To protect against CSRF attacks, you should define a hidden input field holding the CSRF token value that only your website can generate and verify. Hence, the form submissions triggered by the malicious website will fail.
Protecting forms
Once you configure the @adonisjs/shield
package, all form submissions without a CSRF token will automatically fail. Therefore, you must use the csrfField
edge helper to define a hidden input field with the CSRF token.
<form method="POST" action="/">
{{ csrfField() }}
<input type="name" name="name" placeholder="Enter your name">
<button type="submit"> Submit </button>
</form>
<form method="POST" action="/">
<input type="hidden" name="_csrf" value="Q9ghWSf0-3FD9eCiu5YxvKaxLEZ6F_K4DL8o" />
<input type="name" name="name" placeholder="Enter your name" />
<button type="submit">Submit</button>
</form>
During the form submission, the shield_middleware
will automatically verify the _csrf
token, only allowing the form submissions with a valid CSRF token.
Handling exceptions
Shield raises an E_BAD_CSRF_TOKEN
exception when the CSRF token is missing or invalid. By default, AdonisJS will capture the exception and redirect the user back to the form with an error flash message.
You can access the flash message as follows inside an edge template.
@error('E_BAD_CSRF_TOKEN')
<p> {{ $message }} </p>
@end
<form method="POST" action="/">
{{ csrfField() }}
<input type="name" name="name" placeholder="Enter your name">
<button type="submit"> Submit </button>
</form>
You can also self-handle the E_BAD_CSRF_TOKEN
exception inside the global exception handler as follows.
import app from '@adonisjs/core/services/app'
import { errors } from '@adonisjs/shield'
import { HttpContext, ExceptionHandler } from '@adonisjs/core/http'
export default class HttpExceptionHandler extends ExceptionHandler {
async handle(error: unknown, ctx: HttpContext) {
if (error instanceof errors.E_BAD_CSRF_TOKEN) {
return ctx.response
.status(error.status)
.send('Page has expired')
}
return super.handle(error, ctx)
}
}
Config reference
The configuration for the CSRF guard is stored inside the config/shield.ts
file.
import { defineConfig } from '@adonisjs/shield'
const shieldConfig = defineConfig({
csrf: {
enabled: true,
exceptRoutes: [],
enableXsrfCookie: true,
methods: ['POST', 'PUT', 'PATCH', 'DELETE'],
},
})
export default shieldConfig
-
enabled
-
Turn the CSRF guard on or off.
-
exceptRoutes
-
An array of route patterns to exempt from the CSRF protection. If your application has routes that accept form submissions via an API, you might want to exempt them.
For more advanced use cases, you may register a function to exempt specific routes dynamically.
{exceptRoutes: (ctx) => {// exempt all routes starting with /api/return ctx.request.url().includes('/api/')}} -
enableXsrfCookie
-
When enabled, Shield will store the CSRF token inside an encrypted cookie named
XSRF-TOKEN
, which can be read by the frontend JavaScript code.This allows frontend request libraries like Axios to read the
XSRF-TOKEN
automatically and set it as a header when making Ajax requests without server-rendered forms.You must keep the
enableXsrfCookie
disabled if you are not triggering Ajax requests programmatically. -
methods
-
An array of HTTP methods to protect. All incoming requests for the mentioned methods must present a valid CSRF token.
-
cookieOptions
-
Configuration for the
XSRF-TOKEN
cookie. See cookies configuration for available options.
Defining CSP policy
CSP (Content security policy) protects your applications from XSS attacks by defining trusted sources for loading JavaScript, CSS, fonts, images, and so on.
The CSP guard is disabled by default. However, we recommend you enable it and configure the policy directives inside the config/shield.ts
file.
import { defineConfig } from '@adonisjs/shield'
const shieldConfig = defineConfig({
csp: {
enabled: true,
directives: {
// policy directives go here
},
reportOnly: false,
},
})
export default shieldConfig
-
enabled
-
Turn the CSP guard on or off.
-
directives
-
Configure the CSP directives. You can view the list of available directives on https://content-security-policy.com/
const shieldConfig = defineConfig({csp: {enabled: true,directives: {defaultSrc: [`'self'`],scriptSrc: [`'self'`, 'https://cdnjs.cloudflare.com'],fontSrc: [`'self'`, 'https://fonts.googleapis.com']},reportOnly: false,},})export default shieldConfig -
reportOnly
-
The CSP policy will not block the resources when the
reportOnly
flag is enabled. Instead, it will report the violations on an endpoint configured using thereportUri
directive.const shieldConfig = defineConfig({csp: {enabled: true,directives: {defaultSrc: [`'self'`],reportUri: ['/csp-report']},reportOnly: true,},})Also, register the
csp-report
endpoint to collect the violation reports.router.post('/csp-report', async ({ request }) => {const report = request.input('csp-report')})
Using Nonce
You may allow inline script
and style
tags by defining the nonce attribute on them. The value of the nonce attribute can be accessed inside Edge templates using the cspNonce
property.
<script nonce="{{ cspNonce }}">
// Inline JavaScript
</script>
<style nonce="{{ cspNonce }}">
/* Inline CSS */
</style>
Also, use the @nonce
keyword inside the directives config to allow nonce-based inline scripts and styles.
const shieldConfig = defineConfig({
csp: {
directives: {
defaultSrc: [`'self'`, '@nonce'],
},
},
})
Loading assets from the Vite Dev server
If you are using the Vite integration, you can use the following CSP keywords to allow assets served by the Vite Dev server.
- The
@viteDevUrl
adds the Vite dev server URL to the allowed list. - The
@viteHmrUrl
adds the Vite HMR websocket server URL to the allowed list.
const shieldConfig = defineConfig({
csp: {
directives: {
defaultSrc: [`'self'`, '@viteDevUrl'],
connectSrc: ['@viteHmrUrl']
},
},
})
If you are deploying the Vite bundled output to a CDN server, you must replace @viteDevUrl
with the @viteUrl
keyword to allow assets from both the development server and the CDN server.
directives: {
defaultSrc: [`'self'`, '@viteDevUrl'],
defaultSrc: [`'self'`, '@viteUrl'],
connectSrc: ['@viteHmrUrl']
},
Adding Nonce to styles injected by Vite
Currently, Vite does not allow defining a nonce
attribute to the style
tags injected by it inside the DOM. There is an open PR for the same, and we are hoping it will be resolved soon.
Configuring HSTS
The Strict-Transport-Security (HSTS) response header informs the browsers to always load the website using HTTPS.
You can configure the header directives using the config/shield.ts
file.
import { defineConfig } from '@adonisjs/shield'
const shieldConfig = defineConfig({
hsts: {
enabled: true,
maxAge: '180 days',
includeSubDomains: true,
},
})
-
enabled
-
Turn the hsts guard on or off.
-
maxAge
-
Defines the
max-age
attribute. The value should either be a number in seconds or a string-based time expression.{// Remember for 10 secondsmaxAge: 10,}{// Remember for 2 daysmaxAge: '2 days',} -
includeSubDomains
-
Defines the
includeSubDomains
directive to apply the setting on subdomains.
Configuring X-Frame protection
The X-Frame-Options header is used to indicate if a browser is allowed to render a website embedded inside an iframe
, frame
, embed
, or object
tags.
If you have configured CSP, you may instead use the frame-ancestors directive and disable the xFrame
guard.
You can configure the header directives using the config/shield.ts
file.
import { defineConfig } from '@adonisjs/shield'
const shieldConfig = defineConfig({
xFrame: {
enabled: true,
action: 'DENY'
},
})
-
enabled
-
Turn the xFrame guard on or off.
-
action
-
The
action
property defines the header value. It could beDENY
,SAMEORIGIN
, orALLOW-FROM
.{action: 'DENY'}In the case of
ALLOW-FROM
, you must also define thedomain
property.{action: 'ALLOW-FROM',domain: 'https://foo.com',}
Disabling MIME sniffing
The X-Content-Type-Options header instructs browsers to follow the content-type
header and not perform MIME sniffing by inspecting the content of an HTTP response.
Once you enable this guard, Shield will define the X-Content-Type-Options: nosniff
header for all HTTP responses.
import { defineConfig } from '@adonisjs/shield'
const shieldConfig = defineConfig({
contentTypeSniffing: {
enabled: true,
},
})