CSRF Protection
Cross-Site Request Forgery (CSRF) is a type of attack that occurs when a malicious web site, email, blog, instant message, or program causes a user’s web browser to perform an unwanted action on a trusted site when the user is authenticated.
Source: OWASP
There are several ways to defend yourself against a CSRF attack. The better approach is to use both the SameSite
cookie directive and a token-based technique to have an in-depth protection.
Note: CSRF protection only makes sense if your authentication system is based on cookies. This is why there is no example for Mobile+API applications.
SameSite Cookie Attribute
The SameSite
attribute is a new cookie directive to mitigate the risk of CSRF attacks. As of July 2019, SameSite attribute is on browsers used by 86,57% of Internet users.
Authentication with Session Tokens
If you use session tokens with the setSessionCookie
, you can directly define the cookie directives in the configuration.
Example with config/default.json
{
"settings": {
"session": {
"cookie": {
"sameSite": "lax"
}
}
}
}
Example with config/default.yml
settings:
session:
cookie:
sameSite: lax
Authentication with JSON Web Tokens
If you use JSON Web Tokens, then you have to specify the directive manually when sending the token to the browser.
Example
return new HttpResponseOK()
.setCookie('auth', token, {
// ...
sameSite: 'lax'
})
Using CSRF Tokens
npm install @foal/csrf
In addition to the SameSite
directive it is strongly recommended to use a token-based mitigation technique to provide a robust defense.
Here is the principle:
- The server generates a token (stateless or stateful) and sends it to the browser (in the HTML page or in a separate cookie). An attacker performing a CSRF attack is not able to guess or read this token.
- In each subsequent POST, PUT, PATH or DELETE request, the client must include this token in a specific header, in the body of the request or in the URL parameters to prove the "origin" of the request.
- If the CSRF token is not present or is incorrect, the server returns an error 403 - FORBIDDEN with the message
Bad csrf token.
.
FoalTS token-based protection provides a hook and a function to set up the defense.
getCsrfToken
generates or reads the CSRF token.@CsrfTokenRequired
verifies the CSRF token when receiving requests and returns a 403 error if it is missing or incorrect.
The @CsrfTokenRequired
expects the CSRF token to be include in the request in either:
- the request body with the name
_csrf
, - the request query with the name
_csrf
, - or in one of these headers:
CSRF-Token
,XSRF-Token
,X-CSRF-Token
orX-XSRF-Token
.
Regular Web Applications
Regular Web Applications use Server-Side Rendering to generate their HTML pages.
Stateful CSRF token (Session-based)
- Generate the token on login
import { HttpResponseOK, Post, setSessionCookie } from '@foal/core';
class AuthController {
// ...
@Post('/login')
async login() {
// ...
const session = await this.store.createAndSaveSessionFromUser(
user,
// Generate the CSRF token and keep it in the session
{ csrfToken: true }
);
const response = new HttpResponseOK();
setSessionCookie(response, session.getToken());
return response;
}
}
- Include the token in each rendered page.
import { Context, Get, TokenRequired, render } from '@foal/core';
import { TypoORMStore } from '@foal/typeorm';
import { getCsrfToken } from '@foal/csrf';
@TokenRequired({
cookie: true,
redirectTo: '/login',
store: TypeORMStore, // Or another store: RedisStore, MongoDBStore, etc.
})
class PageController {
@Get('/home')
async home(ctx: Context) {
return render(
'./templates/home.html',
// Retreive the token from the session
// and include it in the rendered page
{ csrfToken: await getCsrfToken(ctx.session) }
);
}
}
Home.html (example with a form)
<html>
<head></head>
<body>
<form action="POST">
<input style="display: none" name="_csrf" value="{{ csrfToken }}">
<!--
OR if you use EJS:
<input style="display: none" name="_csrf" value="<%= csrfToken %>">
-->
<input name="foobar">
<button type="submit">Submit</button>
</form>
</body>
</html>
home.html (example with JavaScript)
<html>
<head>
<meta name="csrf-token" content="{{ csrfToken }}">
<!--
OR if you use EJS:
<meta name="csrf-token" content="<%= csrfToken %>">
-->
</head>
<body>
...
<script type="text/javascript">
var csrf_token = document
.querySelector("meta[name='csrf-token']")
.getAttribute("content");
// Add the token in a header (ex: CSRF-TOKEN) when making request
</script>
</body>
</html>
- Check the CSRF token on each subsequent POST, PUT, PATCH and DELETE request.
import { Post, TokenRequired } from '@foal/core';
import { TypeORMStore } from '@foal/typeorm';
import { CsrfTokenRequired } from '@foal/csrf';
@TokenRequired({
cookie: true,
store: TypeORMStore, // Or another store: RedisStore, MongoDBStore, etc.
})
@CsrfTokenRequired()
class ApiController {
@Post('/products')
createProduct() {
return new HttpResponseCreated();
}
}
Stateless CSRF token (Double Submit Cookie Technique)
If you want to use stateless CSRF tokens, you need to provide a base64-encoded secret in either:
-
a configuration file
Example with config/default.yml
settings:
csrf:
secret: xxx -
or in a
.env
file or in an environment variable:SETTINGS_CSRF_SECRET=xxx
You can generate such a secret with the CLI command:
foal createsecret
- Generate a token and send it in a cookie when rendering a page.
import { Context, Get, HttpResponseOK } from '@foal/core';
import { getCsrfToken, setCsrfCookie } from '@foal/csrf';
class PageController {
@Get('/home')
async home(ctx: Context) {
// Normally in an HTML template
const response = new HttpResponseOK();
// Include a random CSRF token in the cookie
setCsrfCookie(response, await getCsrfToken());
return response;
}
}
home.html (example with JavaScript)
<html>
<head>
</head>
<body>
...
<script type="text/javascript">
var csrf_token = // use a library to get the cookie value from document.cookie
// Add the token in a header (ex: CSRF-TOKEN) when making request
</script>
</body>
</html>
- Check the CSRF token on each subsequent POST, PUT, PATCH and DELETE request.
import { HttpResponseCreated, Post } from '@foal/core';
import { CsrfTokenRequired } from '@foal/csrf';
@CsrfTokenRequired({ doubleSubmitCookie: true })
class ApiController {
@Post('/products')
createProduct() {
return new HttpResponseCreated();
}
}
SPA + API
In Single-Page Application + API architecture, the frontend application is static and the pages are rendered in the browser.
First set the configuration key settings.csrf.cookie.maxAge
to a very large number (for example one year).
Example with config/default.json
{
"settings": {
"csrf": {
"cookie": {
"maxAge": 31536000
}
}
}
}
Example with config/default.yml
settings:
csrf:
cookie:
maxAge: 31536000 # One year
Stateful CSRF token (Session-based)
- Generate the token and send it in a cookie on login.
// ...
import { getCsrfToken, setCsrfCookie } from '@foal/csrf';
// ...
class AuthController {
// ...
@Post('/login')
async login() {
// ...
const session = await this.store.createAndSaveSessionFromUser(
user,
// Generate the CSRF token and keep it in the session
{ csrfToken: true }
);
const response = new HttpResponseOK();
setSessionCookie(response, session.getToken());
// Retreive the token from the session
// and send it in a cookie
setCsrfCookie(response, await getCsrfToken(session));
return response;
}
}
Your frontend application then must retreive the token from the cookie named csrfToken
and send it on each subsequent POST, PUT, PATCH or DELETE request (for example using the header CSRF-Token
).
- Check the CSRF token on each subsequent POST, PUT, PATCH and DELETE request.
import { HttpResponseCreated, Post, TokenRequired } from '@foal/core';
import { TypeORMStore } from '@foal/typeorm';
import { CsrfTokenRequired } from '@foal/csrf';
@TokenRequired({
cookie: true,
store: TypeORMStore, // Or another store: RedisStore, MongoDBStore, etc.
})
@CsrfTokenRequired()
class ApiController {
@Post('/products')
createProduct() {
return new HttpResponseCreated();
}
}
Stateless CSRF token (Double Submit Cookie Technique)
If you want to use stateless CSRF tokens, you need to provide a base64-encoded secret in either:
-
a configuration file
Example with config/default.yml
settings:
csrf:
secret: xxx
cookie:
maxAge: 31536000 # One year -
or in a
.env
file or in an environment variable:SETTINGS_CSRF_SECRET=xxx
You can generate such a secret with the CLI command:
foal createsecret
- Generate a token and send it in a cookie on login.
import { HttpResponseOK, Post } from '@foal/core';
import { getCsrfToken, setCsrfCookie } from '@foal/csrf';
class AuthController {
@Post('/login')
async login() {
const response = new HttpResponseOK();
setCsrfCookie(response, await getCsrfToken());
return response;
}
}
Your frontend application then must retreive the token from the cookie named csrfToken
and send it on each subsequent POST, PUT, PATCH or DELETE request (for example using the header CSRF-Token
).
- Check the CSRF token on each subsequent POST, PUT, PATCH and DELETE request.
import { HttpResponseCreated, Post } from '@foal/core';
import { CsrfTokenRequired } from '@foal/csrf';
@CsrfTokenRequired({ doubleSubmitCookie: true })
class ApiController {
@Post('/products')
createProduct() {
return new HttpResponseCreated();
}
}
Disable the CSRF protection
The CSRF hook @CsrfTokenRequired
can be disabled on a specific environment using the configuration key settings.csrf.enabled
.
Example with config/test.json
{
"settings": {
"csrf": {
"enabled": false
}
}
}
Example with config/test.yml
settings:
csrf:
enabled: false
Example with environment variable
SETTINGS_CSRF_ENABLED=false
Advanced
The directives of the cookie written by setCsrfCookie
can be override in the configuration.
Example with config/default.yml
settings:
csrf:
cookie:
name: my-custom-name
domain: example.com
path: /foo # default: /
sameSite: lax
secure: true
maxAge: 10000