Cross-Site Request Forgery(CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they are currently authenticated.
Quarkus Security provides a CSRF prevention feature which consists of a Resteasy Reactive server filter which creates and verifies CSRF tokens and an HTML form parameter provider which supports the injection of CSRF tokens in Qute templates.
Creating the Project
First, we need a new project. Create a new project with the following command:
This command generates a project which imports the csrf-reactive
extension.
If you already have your Quarkus project configured, you can add the csrf-reactive
extension
to your project by running the following command in your project base directory:
quarkus extension add 'csrf-reactive'
./mvnw quarkus:add-extension -Dextensions="csrf-reactive"
./gradlew addExtension --extensions="csrf-reactive"
This will add the following to your build file:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-csrf-reactive</artifactId>
</dependency>
implementation("io.quarkus:quarkus-csrf-reactive")
Next lets add a Qute template producing an HTML form:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>User Name Input</title>
</head>
<body>
<h1>User Name Input</h1>
<form action="/service/csrfTokenForm" method="post">
<input type="hidden" name="{inject:csrf.parameterName}" value="{inject:csrf.token}" /> (1)
<p>Your Name: <input type="text" name="name" /></p>
<p><input type="submit" name="submit"/></p>
</form>
</body>
</html>
1 | This expression is used to inject a CSRF token into a hidden form field. This token will be verified by the CSRF filter against a CSRF cookie. |
You can name the file containing this template as csrfToken.html
and put it in a src/main/resources/templates
folder.
Now let’s create a resource class which returns an HTML form and handles form POST requests:
package io.quarkus.it.csrf;
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
@Path("/service")
public class UserNameResource {
@Inject
Template csrfToken; (1)
@GET
@Path("/csrfTokenForm")
@Produces(MediaType.TEXT_HTML)
public TemplateInstance getCsrfTokenForm() {
return csrfToken.instance(); (2)
}
@POST
@Path("/csrfTokenForm")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.TEXT_PLAIN)
public String postCsrfTokenForm(@FormParam("name") String name) {
return userName; (3)
}
}
1 | Inject the csrfToken.html as a Template . |
2 | Return HTML form with a hidden form field containing a CSRF token created by the CSRF filter. |
3 | Handle the form POST request, this method can only be invoked only if the CSRF filter has successfully verified the token. |
The form POST request will fail with HTTP status 400
if the filter finds the hidden CSRF form field is missing, the CSRF cookie is missing, or if the CSRF form field and CSRF cookie values do not match.
At this stage no additional configuration is needed - by default the CSRF form field and cookie name will be set to csrf_token
, and the filter will verify the token. But lets change these names:
quarkus.csrf-reactive.form-field-name=csrftoken
quarkus.csrf-reactive.cookie-name=csrftoken
Note that the CSRF filter has to read the input stream in order to verify the token and then re-create the stream for the application code to read it as well. The filter performs this work on an event loop thread so for small form payloads such as the one shown in the example above it will have negligible peformance side-effects. However if you deal with large form payloads then it is recommended to compare the CSRF form field and cookie values in the application code:
package io.quarkus.it.csrf;
import javax.inject.Inject;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.Consumes;
import javax.ws.rs.CookieParam;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
@Path("/service")
public class UserNameResource {
@Inject
Template csrfToken;
@GET
@Path("/csrfTokenForm")
@Produces(MediaType.TEXT_HTML)
public TemplateInstance getCsrfTokenForm() {
return csrfToken.instance();
}
@POST
@Path("/csrfTokenForm")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.TEXT_PLAIN)
public String postCsrfTokenForm(@CookieParam("csrf-token") csrfCookie, @FormParam("csrf-token") String formCsrfToken, @FormParam("name") String userName) {
if (!csrfCookie.getValue().equals(formCsrfToken)) { (1)
throw new BadRequestException();
}
return userName;
}
}
1 | Compare the CSRF form field and cookie values and fail with HTTP status 400 if they don’t match. |
Also disable the token verification in the filter:
quarkus.csrf-reactive.verify-token=false
Configuration Reference
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Type |
Default |
|
---|---|---|
If filter is enabled. Environment variable: |
boolean |
|
Form field name which keeps a CSRF token. Environment variable: |
string |
|
CSRF cookie name. Environment variable: |
string |
|
CSRF cookie max age. Environment variable: |
|
|
CSRF cookie path. Environment variable: |
string |
|
CSRF cookie domain. Environment variable: |
string |
|
If enabled the CSRF cookie will have its 'secure' parameter set to 'true' when HTTP is used. It may be necessary when running behind an SSL terminating reverse proxy. The cookie will always be secure if HTTPS is used even if this property is set to false. Environment variable: |
boolean |
|
Create CSRF token only if the HTTP GET relative request path is the same as the one configured with this property. Environment variable: |
string |
|
The random CSRF token size in bytes. Environment variable: |
int |
|
Verify CSRF token in the CSRF filter. If this property is enabled then the input stream will be read by the CSRF filter to verify the token and recreated for the application code to read the data correctly. Therefore, it is recommended to disable this property when dealing with the large form payloads and instead compare CSRF form and cookie parameters in the application code using JAX-RS Environment variable: |
boolean |
|
Require that only 'application/x-www-form-urlencoded' body is accepted for the token verification to proceed. Disable this property for the CSRF filter to avoid verifying the token for POST requests with other content types. This property is only effective if Environment variable: |
boolean |
|
About the Duration format
The format for durations uses the standard You can also provide duration values starting with a number.
In this case, if the value consists only of a number, the converter treats the value as seconds.
Otherwise, |