跨站点请求伪造防御
跨站请求伪造 (CSRF) 是一种攻击,它会迫使最终用户在其当前已认证的 Web 应用程序中执行非预期的操作。
Quarkus Security 提供了一项 CSRF 防护功能,该功能实现了“双重提交 Cookie”和“CSRF 请求头”技术。
“双重提交 Cookie”技术要求将 CSRF 令牌作为 HTTPOnly
(可选签名)Cookie 发送到客户端,并直接嵌入到服务器端渲染的 HTML 表单的隐藏表单输入字段中,或者作为请求头的值提交。
该扩展包含一个 Quarkus REST (前身为 RESTEasy Reactive) 服务器过滤器,用于创建和验证 application/x-www-form-urlencoded
和 multipart/form-data
表单中的 CSRF 令牌;以及一个 Qute HTML 表单参数提供程序,用于支持将 CSRF 令牌注入 Qute 模板。
创建项目
首先,我们需要一个新项目。使用以下命令创建一个新项目
对于 Windows 用户
-
如果使用 cmd,(不要使用反斜杠
\
并将所有内容放在同一行上) -
如果使用 Powershell,请将
-D
参数用双引号括起来,例如"-DprojectArtifactId=security-csrf-prevention"
此命令将生成一个导入 rest-csrf
扩展的项目。
如果您已经配置了 Quarkus 项目,可以通过在项目根目录中运行以下命令将 rest-csrf
扩展添加到项目中:
quarkus extension add rest-csrf
./mvnw quarkus:add-extension -Dextensions='rest-csrf'
./gradlew addExtension --extensions='rest-csrf'
这会将以下内容添加到您的构建文件中
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-csrf</artifactId>
</dependency>
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-urlencoded
或 multipart/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
配置参考
构建时固定的配置属性 - 所有其他配置属性都可以在运行时覆盖
配置属性 |
类型 |
默认 |
---|---|---|
布尔值 |
|
|
字符串 |
|
|
字符串 |
|
|
字符串 |
|
|
|
||
字符串 |
|
|
字符串 |
||
如果启用,在使用 HTTP 时,CSRF Cookie 将设置其“secure”参数为“true”。当运行在 SSL 终止反向代理后面时,这可能是必要的。即使此属性设置为 false,只要使用 HTTPS,Cookie 总是安全的。 环境变量: 显示更多 |
布尔值 |
|
设置 HttpOnly 属性以防止通过 JavaScript 访问 cookie。 环境变量: 显示更多 |
布尔值 |
|
仅当 HTTP GET 相对请求路径匹配此属性配置的一个路径时,才创建 CSRF 令牌。使用逗号分隔多个路径值。 环境变量: 显示更多 |
字符串列表 |
|
整数 |
|
|
CSRF 令牌 HMAC 签名密钥,如果设置了此密钥,则该密钥必须至少包含 32 个字符。 环境变量: 显示更多 |
字符串 |
|
在 CSRF 过滤器中验证 CSRF 令牌。如果您愿意,可以禁用此属性,并在应用程序代码中使用 JAX-RS jakarta.ws.rs.FormParam(引用 环境变量: 显示更多 |
布尔值 |
|
要求仅接受 'application/x-www-form-urlencoded' 或 'multipart/form-data' 主体才能继续令牌验证。为 CSRF 过滤器禁用此属性,以避免验证其他内容类型的 POST 请求的令牌。此属性仅在 环境变量: 显示更多 |
布尔值 |
|
关于 Duration 格式
要编写 duration 值,请使用标准的 您还可以使用简化的格式,以数字开头
在其他情况下,简化格式将被转换为
|