Cross-Site Request Forgery Prevention

Cross-Site Request Forgery (CSRF) 是一种攻击,它迫使用户在他们当前已通过身份验证的 Web 应用程序中执行不需要的操作。 Quarkus Security 提供了一项 CSRF 防护功能,该功能实现了 Double Submit CookieCSRF Request Header 技术。 Double Submit Cookie 技术要求 CSRF 令牌以 HTTPOnly 的形式发送,可以签名,通过 Cookie 发送到客户端,直接嵌入到服务端呈现的 HTML 表单的隐藏表单输入中,或者作为请求头值提交。 该扩展包括一个 Quarkus REST (formerly RESTEasy Reactive) 服务器过滤器,该过滤器在 application/x-www-form-urlencodedmultipart/form-data 表单中创建并验证 CSRF 令牌,以及一个 Qute HTML 表单参数提供程序,该提供程序支持 injection of CSRF tokens in Qute templates

Creating the Project

首先,我们需要一个新项目。使用以下命令创建一个新项目:

CLI
quarkus create app {create-app-group-id}:{create-app-artifact-id} \
    --no-code
cd {create-app-artifact-id}

要创建一个 Gradle 项目,添加 --gradle--gradle-kotlin-dsl 选项。 有关如何安装和使用 Quarkus CLI 的详细信息,请参见 Quarkus CLI 指南。

Maven
mvn {quarkus-platform-groupid}:quarkus-maven-plugin:{quarkus-version}:create \
    -DprojectGroupId={create-app-group-id} \
    -DprojectArtifactId={create-app-artifact-id} \
    -DnoCode
cd {create-app-artifact-id}

要创建一个 Gradle 项目,添加 -DbuildTool=gradle-DbuildTool=gradle-kotlin-dsl 选项。

适用于 Windows 用户:

  • 如果使用 cmd,(不要使用反斜杠 \ ,并将所有内容放在同一行上)

  • 如果使用 Powershell,将 -D 参数用双引号引起来,例如 "-DprojectArtifactId={create-app-artifact-id}"

此命令生成一个导入 csrf-reactive 扩展的项目。

如果您已配置 Quarkus 项目,则可以通过在项目基本目录中运行以下命令将 csrf-reactive 扩展添加到您的项目:

CLI
quarkus extension add {add-extension-extensions}
Maven
./mvnw quarkus:add-extension -Dextensions='{add-extension-extensions}'
Gradle
./gradlew addExtension --extensions='{add-extension-extensions}'

这会将以下内容添加到构建文件中:

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

接下来,让我们添加一个 csrfToken.html Qute 模板,在 src/main/resources/templates 文件夹中生成一个 HTML 表单:

<!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 此表达式用于将 CSRF 令牌注入一个隐藏表单字段。该令牌将由 CSRF 过滤器使用 CSRF cookie 进行验证。

现在,让我们创建一个资源类,该类返回一个 HTML 表单并处理表单 POST 请求:

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 注入 csrfToken.html 作为 Template
2 返回包含 CSRF 过滤器创建的 CSRF 令牌的隐藏表单字段的 HTML 表单。
3 处理 POST 表单请求,此方法只能在 CSRF 过滤器已成功验证令牌的情况下调用。

如果过滤器发现隐藏的 CSRF 表单字段缺失、CSRF cookie 缺失,或 CSRF 表单字段和 CSRF cookie 的值不匹配,表单 POST 请求将失败,HTTP 状态为 400

在这一阶段,不需要任何其他配置 - 默认情况下,CSRF 表单字段和 cookie 名称将设置为 csrf-token ,过滤器将验证令牌。但是,如果您愿意,可以更改这些名称:

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

Sign CSRF token

您可以为生成的 CSRF 令牌获取创建的 HMAC 签名,并如果不想承担攻击者重新创建 CSRF 令牌的风险,可以将这些 HMAC 值存储为 CSRF 令牌 Cookie。您需要做的就是配置令牌签名密钥,密钥长度必须至少为 32 个字符:

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

CSRF Request Header

如果未使用 HTML form 标签,而您需要将 CSRF 令牌作为标题传递,则将标题名称和令牌注入到 HTMX 中,例如:

<body hx-headers='{"{inject:csrf.headerName}":"{inject:csrf.token}"}'> 1
</body>
1 此表达式用于注入 CSRF 令牌标头和令牌。CSRF 过滤器会根据 CSRF Cookie 检查此令牌。

默认标题名称是 X-CSRF-TOKEN,您可以使用 quarkus.rest-csrf.token-header-name 自定义它,例如:

quarkus.rest-csrf.token-header-name=CUSTOM-X-CSRF-TOKEN

如果您需要从 JavaScript 访问 CSRF Cookie 以将其值作为标题传递,请使用 {inject:csrf.cookieName}{inject:csrf.headerName} 注入必须作为 CSRF 标头值读取的 Cookie 名称,并允许访问此 Cookie:

quarkus.rest-csrf.cookie-http-only=false

Cross-origin resource sharing

如果您想要在一个跨域环境中实施 CSRF 保护,请避免支持所有域。 将受支持的域限制为仅受信任的域,有关更多信息,请参阅“跨域资源共享”指南的 CORS filter 部分。

Restrict CSRF token verification

Jakarta REST 端点不仅可以接受带有 application/x-www-form-urlencodedmultipart/form-data 有效负载的 HTTP POST 请求,还可以接受具有其他媒体类型且路径 URL 相同或不同的有效负载,因此您希望避免在这种情况下验证 CSRF 令牌,例如:

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 表单请求至 /user,CSRF 令牌验证由 CSRF 过滤器实施
2 POST json 请求至 /user,不需要 CSRF 令牌验证
3 POST json 请求至 /users,不需要 CSRF 令牌验证

正如您所见,将在接受 application/x-www-form-urlencoded 有效负载的 /service/user 路径上需要 CSRF 令牌验证,但是发送到 /service/user/service/users 方法的 User JSON 表示形式将不具有 CSRF 令牌,因此必须在这些情况下通过将其限制为特定的 /service/user 请求路径但在这个路径上不仅允许 application/x-www-form-urlencoded 来跳过令牌验证:

# Verify CSRF token only for the `/service/user` path, ignore other paths such as `/service/users`
quarkus.rest-csrf.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
# Setting this property is not necessary when the token is submitted as a header value
quarkus.rest-csrf.require-form-url-encoded=false

Verify CSRF token in the application code

如果您希望在应用程序代码中比较 CSRF 表单字段和 Cookie 值,则可以执行以下操作:

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 比较 CSRF 表单字段和 Cookie 值,如果不匹配,则以 HTTP 状态 400 失败。

还在过滤器中禁用令牌验证:

quarkus.rest-csrf.verify-token=false

Configuration Reference

Unresolved include directive in modules/ROOT/pages/security-csrf-prevention.adoc - include::../../../target/quarkus-generated-doc/config/quarkus-rest-csrf.adoc[]