Skip to content

Latest commit

 

History

History
294 lines (232 loc) · 10 KB

security-csrf-prevention.adoc

File metadata and controls

294 lines (232 loc) · 10 KB

Cross-Site Request Forgery Prevention

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 implements a Double Submit Cookie technique. This techninque requires that the CSRF token is never directly exposed to scripts executed on the client-side. In this extension, the CSRF token is:

  • sent as HTTPOnly cookie to the client, and

  • directly embedded in a hidden form input of server-side rendered forms, which are transmitted to and used by the client.

The extension consists of a RESTEasy Reactive server filter which creates and verifies CSRF tokens in application/x-www-form-urlencoded and multipart/form-data forms and a Qute 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:

This will add the following to your build file:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-csrf-reactive</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-csrf-reactive")

Next, let’s add a csrfToken.html Qute template producing an HTML form in the src/main/resources/templates folder:

<!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.

Now let’s create a resource class which returns an HTML form and handles form POST requests:

package io.quarkus.it.csrf;

import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.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 userName) {
        return userName; (3)
    }
}
  1. Inject the csrfToken.html as a Template.

  2. Return the HTML form with a hidden form field containing a CSRF token created by the CSRF filter.

  3. Handle the POST form request, this method can only be invoked 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 you can change these names if you would like:

quarkus.csrf-reactive.form-field-name=csrftoken
quarkus.csrf-reactive.cookie-name=csrftoken

Sign CSRF token

You can get HMAC signatures created for the generated CSRF tokens and have these HMAC values stored as CSRF token cookies if you would like to avoid the risk of the attackers recreating the CSRF cookie token. All you need to do is to configure a token signature secret which must be at least 32 characters long:

quarkus.csrf-reactive.token-signature-key=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow

Restrict CSRF token verification

Your Jakarta REST endpoint may accept not only HTTP POST requests with application/x-www-form-urlencoded or multipart/form-data payloads but also payloads with other media types, either on the same or different URL paths, and therefore you would like to avoid verifying the CSRF token in such cases, for example:

package io.quarkus.it.csrf;

import jakarta.inject.Inject;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;

@Path("/service")
public class UserNameResource {

    @Inject
    Template csrfToken;

    @GET
    @Path("/user")
    @Produces(MediaType.TEXT_HTML)
    public TemplateInstance getCsrfTokenForm() {
        return csrfToken.instance();
    }

    (1)
    @POST
    @Path("/user")
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Produces(MediaType.TEXT_PLAIN)
    public String postCsrfTokenForm(@FormParam("name") String userName) {
        return userName;
    }

    (2)
    @POST
    @Path("/user")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.TEXT_PLAIN)
    public String postJson(User user) {
        return user.name;
    }

    (3)
    @POST
    @Path("/users")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.TEXT_PLAIN)
    public String postJson(User user) {
        return user.name;
    }

    public static class User {
        private String name;
        public String getName() {
            return this.name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
}
  1. POST form request to /user, CSRF token verification is enforced by the CSRF filter

  2. POST json request to /user, CSRF token verification is not needed

  3. POST json request to /users, CSRF token verification is not needed

As you can see a CSRF token verification will be required at the /service/user path accepting the application/x-www-form-urlencoded payload, but User JSON representation posted to both /service/user and /service/users method will have no CSRF token and therefore the token verification has to be skipped in these cases by restricting it to the specific /service/user request path but also allowing not only application/x-www-form-urlencoded on this path:

# Verify CSRF token only for the `/service/user` path, ignore other paths such as `/service/users`
quarkus.csrf-reactive.create-token-path=/service/user

# If `/service/user` path accepts not only `application/x-www-form-urlencoded` payloads but also other ones such as JSON then allow them
quarkus.csrf-reactive.require-form-url-encoded=false

Verify CSRF token in the application code

If you prefer to compare the CSRF form field and cookie values in the application code then you can do it as follows:

package io.quarkus.it.csrf;

import jakarta.inject.Inject;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.CookieParam;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Cookie;
import jakarta.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") Cookie 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