编辑此页面

跨站点请求伪造防御

跨站请求伪造 (CSRF) 是一种攻击,它会迫使最终用户在其当前已认证的 Web 应用程序中执行非预期的操作。

Quarkus Security 提供了一项 CSRF 防护功能,该功能实现了“双重提交 Cookie”和“CSRF 请求头”技术。

“双重提交 Cookie”技术要求将 CSRF 令牌作为 HTTPOnly(可选签名)Cookie 发送到客户端,并直接嵌入到服务器端渲染的 HTML 表单的隐藏表单输入字段中,或者作为请求头的值提交。

该扩展包含一个 Quarkus REST (前身为 RESTEasy Reactive) 服务器过滤器,用于创建和验证 application/x-www-form-urlencodedmultipart/form-data 表单中的 CSRF 令牌;以及一个 Qute HTML 表单参数提供程序,用于支持将 CSRF 令牌注入 Qute 模板。

创建项目

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

CLI
quarkus create app org.acme:security-csrf-prevention \
    --extension='rest-csrf' \
    --no-code
cd security-csrf-prevention

要创建 Gradle 项目,请添加 --gradle--gradle-kotlin-dsl 选项。

有关如何安装和使用 Quarkus CLI 的更多信息,请参阅 Quarkus CLI 指南。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:3.24.4:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=security-csrf-prevention \
    -Dextensions='rest-csrf' \
    -DnoCode
cd security-csrf-prevention

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

对于 Windows 用户

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

  • 如果使用 Powershell,请将 -D 参数用双引号括起来,例如 "-DprojectArtifactId=security-csrf-prevention"

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

如果您已经配置了 Quarkus 项目,可以通过在项目根目录中运行以下命令将 rest-csrf 扩展添加到项目中:

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

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

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

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

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

签名 CSRF 令牌

您可以获取为生成的 CSRF 令牌创建的 HMAC 签名,并将这些 HMAC 值存储为 CSRF 令牌 Cookie,以避免攻击者重新创建 CSRF Cookie 令牌的风险。您只需要配置一个令牌签名密钥,该密钥必须至少包含 32 个字符。

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

CSRF 请求头

如果未使用 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

跨域资源共享

如果您想在跨域环境中强制执行 CSRF 防护,请避免支持所有 Origin。

将支持的 Origin 限制为仅受信任的 Origin。有关更多信息,请参阅“跨域资源共享”指南的“CORS 过滤器”部分。

限制 CSRF 令牌验证

您的 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

在应用程序代码中验证 CSRF 令牌

如果您希望在应用程序代码中比较 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

配置参考

构建时固定的配置属性 - 所有其他配置属性都可以在运行时覆盖

配置属性

类型

默认

如果过滤器已启用。

环境变量:QUARKUS_REST_CSRF_ENABLED

显示更多

布尔值

true

保存 CSRF 令牌的表单字段名称。

环境变量:QUARKUS_REST_CSRF_FORM_FIELD_NAME

显示更多

字符串

csrf-token

提供 CSRF 令牌的令牌头。

环境变量:QUARKUS_REST_CSRF_TOKEN_HEADER_NAME

显示更多

字符串

X-CSRF-TOKEN

CSRF Cookie 名称。

环境变量:QUARKUS_REST_CSRF_COOKIE_NAME

显示更多

字符串

csrf-token

CSRF Cookie 最大年龄。

环境变量:QUARKUS_REST_CSRF_COOKIE_MAX_AGE

显示更多

Duration 

2H

CSRF Cookie 路径。

环境变量:QUARKUS_REST_CSRF_COOKIE_PATH

显示更多

字符串

/

CSRF Cookie 域名。

环境变量:QUARKUS_REST_CSRF_COOKIE_DOMAIN

显示更多

字符串

如果启用,在使用 HTTP 时,CSRF Cookie 将设置其“secure”参数为“true”。当运行在 SSL 终止反向代理后面时,这可能是必要的。即使此属性设置为 false,只要使用 HTTPS,Cookie 总是安全的。

环境变量:QUARKUS_REST_CSRF_COOKIE_FORCE_SECURE

显示更多

布尔值

false

设置 HttpOnly 属性以防止通过 JavaScript 访问 cookie。

环境变量:QUARKUS_REST_CSRF_COOKIE_HTTP_ONLY

显示更多

布尔值

true

仅当 HTTP GET 相对请求路径匹配此属性配置的一个路径时,才创建 CSRF 令牌。使用逗号分隔多个路径值。

环境变量:QUARKUS_REST_CSRF_CREATE_TOKEN_PATH

显示更多

字符串列表

随机 CSRF 令牌大小(以字节为单位)。

环境变量:QUARKUS_REST_CSRF_TOKEN_SIZE

显示更多

整数

16

CSRF 令牌 HMAC 签名密钥,如果设置了此密钥,则该密钥必须至少包含 32 个字符。

环境变量:QUARKUS_REST_CSRF_TOKEN_SIGNATURE_KEY

显示更多

字符串

在 CSRF 过滤器中验证 CSRF 令牌。如果您愿意,可以禁用此属性,并在应用程序代码中使用 JAX-RS jakarta.ws.rs.FormParam(引用 form-field-name 表单属性)和 jakarta.ws.rs.CookieParam(引用 RestCsrfConfig#cookieName Cookie)来比较 CSRF 表单和 Cookie 参数。请注意,即使禁用了 CSRF 过滤器中的 CSRF 令牌验证,过滤器仍将执行检查,以确保令牌可用、具有正确的 token-size(以字节为单位),并且 Content-Type HTTP 头为“application/x-www-form-urlencoded”或“multipart/form-data”。

环境变量:QUARKUS_REST_CSRF_VERIFY_TOKEN

显示更多

布尔值

true

要求仅接受 'application/x-www-form-urlencoded' 或 'multipart/form-data' 主体才能继续令牌验证。为 CSRF 过滤器禁用此属性,以避免验证其他内容类型的 POST 请求的令牌。此属性仅在 verify-token 属性启用且 token-header-name 未配置时有效。

环境变量:QUARKUS_REST_CSRF_REQUIRE_FORM_URL_ENCODED

显示更多

布尔值

true

关于 Duration 格式

要编写 duration 值,请使用标准的 java.time.Duration 格式。有关更多信息,请参阅 Duration#parse() Java API 文档

您还可以使用简化的格式,以数字开头

  • 如果该值仅为一个数字,则表示以秒为单位的时间。

  • 如果该值是一个数字后跟 ms,则表示以毫秒为单位的时间。

在其他情况下,简化格式将被转换为 java.time.Duration 格式以进行解析

  • 如果该值是一个数字后跟 hms,则在其前面加上 PT

  • 如果该值是一个数字后跟 d,则在其前面加上 P

相关内容