Cross-Site Request Forgery Prevention
Cross-Site Request Forgery (CSRF) 是一种攻击,它迫使用户在他们当前已通过身份验证的 Web 应用程序中执行不需要的操作。
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 提供了一项 CSRF 防护功能,该功能实现了 Double Submit Cookie 和 CSRF Request Header 技术。
Quarkus Security provides a CSRF prevention feature which implements Double Submit Cookie and CSRF Request Header techniques.
Double Submit Cookie
技术要求 CSRF 令牌以 HTTPOnly
的形式发送,可以签名,通过 Cookie 发送到客户端,直接嵌入到服务端呈现的 HTML 表单的隐藏表单输入中,或者作为请求头值提交。
Double Submit Cookie
technique requires that the CSRF token sent as HTTPOnly
, optionally signed, cookie to the client, and
directly embedded in a hidden form input of server-side rendered HTML forms, or submitted as a request header value.
该扩展包括一个 Quarkus REST (formerly RESTEasy Reactive) 服务器过滤器,该过滤器在 application/x-www-form-urlencoded
和 multipart/form-data
表单中创建并验证 CSRF 令牌,以及一个 Qute HTML 表单参数提供程序,该提供程序支持 injection of CSRF tokens in Qute templates 。
The extension consists of a Quarkus REST (formerly 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:
Unresolved directive in security-csrf-prevention.adoc - include::{includes}/devtools/create-app.adoc[]
此命令生成一个导入 csrf-reactive
扩展的项目。
This command generates a project which imports the csrf-reactive
extension.
如果您已配置 Quarkus 项目,则可以通过在项目基本目录中运行以下命令将 csrf-reactive
扩展添加到您的项目:
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:
Unresolved directive in security-csrf-prevention.adoc - include::{includes}/devtools/extension-add.adoc[]
这会将以下内容添加到构建文件中:
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")
接下来,让我们添加一个 csrfToken.html
Qute 模板,在 src/main/resources/templates
文件夹中生成一个 HTML 表单:
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. |
现在,让我们创建一个资源类,该类返回一个 HTML 表单并处理表单 POST 请求:
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. |
如果过滤器发现隐藏的 CSRF 表单字段缺失、CSRF cookie 缺失,或 CSRF 表单字段和 CSRF cookie 的值不匹配,表单 POST 请求将失败,HTTP 状态为 400
。
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.
在这一阶段,不需要任何其他配置 - 默认情况下,CSRF 表单字段和 cookie 名称将设置为 csrf-token
,过滤器将验证令牌。但是,如果您愿意,可以更改这些名称:
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.rest-csrf.form-field-name=csrftoken
quarkus.rest-csrf.cookie-name=csrftoken
Sign CSRF token
您可以为生成的 CSRF 令牌获取创建的 HMAC
签名,并如果不想承担攻击者重新创建 CSRF 令牌的风险,可以将这些 HMAC
值存储为 CSRF 令牌 Cookie。您需要做的就是配置令牌签名密钥,密钥长度必须至少为 32 个字符:
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.rest-csrf.token-signature-key=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow
CSRF Request Header
如果未使用 HTML form
标签,而您需要将 CSRF 令牌作为标题传递,则将标题名称和令牌注入到 HTMX 中,例如:
If HTML form
tags are not used and you need to pass CSRF token as a header, then inject the header name and token, for example, into HTMX:
<body hx-headers='{"{inject:csrf.headerName}":"{inject:csrf.token}"}'> 1
</body>
1 | This expression is used to inject a CSRF token header and token. This token will be verified by the CSRF filter against a CSRF cookie. |
默认标题名称是 X-CSRF-TOKEN
,您可以使用 quarkus.rest-csrf.token-header-name
自定义它,例如:
Default header name is X-CSRF-TOKEN
, you can customize it with quarkus.rest-csrf.token-header-name
, for example:
quarkus.rest-csrf.token-header-name=CUSTOM-X-CSRF-TOKEN
如果您需要从 JavaScript 访问 CSRF Cookie 以将其值作为标题传递,请使用 {inject:csrf.cookieName}
和 {inject:csrf.headerName}
注入必须作为 CSRF 标头值读取的 Cookie 名称,并允许访问此 Cookie:
If you need to access the CSRF cookie from JavaScript in order to pass its value as a header, use {inject:csrf.cookieName}
and {inject:csrf.headerName}
to inject the cookie name which has to be read as a CSRF header value and allow accessing this cookie:
quarkus.rest-csrf.cookie-http-only=false
Cross-origin resource sharing
如果您想要在一个跨域环境中实施 CSRF 保护,请避免支持所有域。 If you would like to enforce CSRF prevention in a Cross-origin environment, please avoid supporting all Origins. 将受支持的域限制为仅受信任的域,有关更多信息,请参阅“跨域资源共享”指南的 CORS filter 部分。 Restrict supported Origins to trusted Origins only, see CORS filter section of the "Cross-origin resource sharing" guide for more information. |
Restrict CSRF token verification
Jakarta REST 端点不仅可以接受带有 application/x-www-form-urlencoded
或 multipart/form-data
有效负载的 HTTP POST 请求,还可以接受具有其他媒体类型且路径 URL 相同或不同的有效负载,因此您希望避免在这种情况下验证 CSRF 令牌,例如:
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 |
正如您所见,将在接受 application/x-www-form-urlencoded
有效负载的 /service/user
路径上需要 CSRF 令牌验证,但是发送到 /service/user
和 /service/users
方法的 User
JSON 表示形式将不具有 CSRF 令牌,因此必须在这些情况下通过将其限制为特定的 /service/user
请求路径但在这个路径上不仅允许 application/x-www-form-urlencoded
来跳过令牌验证:
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.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 值,则可以执行以下操作:
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.rest-csrf.verify-token=false