防范跨站请求伪造
跨站请求伪造(CSRF)会诱使用户在已经登录的Web应用中执行并非其本意的操作。
Quarkus Security通过双重提交Cookie与CSRF请求头技术提供CSRF防护。
双重提交Cookie要求把CSRF令牌作为HttpOnly Cookie发送给客户端,Cookie可选择签名;同时把令牌直接嵌入服务端渲染HTML表单的隐藏字段,或者作为请求头值提交。
|
如果需要不由服务器创建Cookie的无状态CSRF防护,可查看CORS过滤器。尽管名为CORS,它也会检查请求 |
该扩展包含Quarkus REST(原RESTEasy Reactive)服务器过滤器,用于创建、验证application/x-www-form-urlencoded与multipart/form-data表单中的CSRF令牌;还包含Qute HTML表单参数提供器,支持在Qute模板中注入令牌。
过滤器应用于HTTP POST、PUT、PATCH、DELETE,以及其他可能改变REST应用状态的方法。
创建项目
首先创建一个新项目:
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.40.1: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目录中加入csrfToken.html Qute模板,用于生成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 | 返回HTML表单,其中隐藏字段包含过滤器创建的CSRF令牌。 |
| 3 | 处理POST表单请求;只有过滤器成功验证令牌后,才会调用此方法。 |
如果缺少隐藏CSRF表单字段、缺少CSRF Cookie,或二者值不匹配,POST表单请求会以HTTP 400失败。
此时不需要额外配置:默认表单字段和Cookie名称均为csrf-token,过滤器默认验证令牌。也可以修改这些名称:
quarkus.rest-csrf.form-field-name=csrftoken
quarkus.rest-csrf.cookie-name=csrftoken
对 CSRF 令牌签名
如果希望降低攻击者重建CSRF Cookie令牌的风险,可以为生成的令牌创建HMAC签名,并把HMAC值存入CSRF Cookie。只需配置一个至少32字符的签名密钥:
quarkus.rest-csrf.token-signature-key=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow
CSRF 请求头
如果不使用HTML表单,而需要通过请求头传递令牌,可以把请求头名称和令牌注入HTMX等代码中:
<body hx-headers='{"{inject:csrf.headerName}":"{inject:csrf.token}"}'> (1)
</body>
| 1 | 该表达式注入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}注入相应名称,并允许脚本访问该Cookie:
quarkus.rest-csrf.cookie-http-only=false
跨源资源共享
|
在跨源环境中实施CSRF防护时,请避免允许所有Origin。 应只允许受信任Origin,详情参见跨源资源共享指南中的CORS过滤器章节。 |
限制 CSRF 令牌验证范围
Jakarta REST端点可能同时接受application/x-www-form-urlencoded、multipart/form-data及其他媒体类型的POST负载,这些请求可能使用相同或不同路径。对于某些非表单请求,可能希望不验证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令牌。 |
| 2 | POST JSON到/user:不需要验证CSRF令牌。 |
| 3 | POST JSON到/users:不需要验证CSRF令牌。 |
接受application/x-www-form-urlencoded的/service/user需要验证令牌;发送到/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 令牌
如果希望在应用代码中比较表单字段与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
配置参考
带标记的配置在构建时固定;其余配置可以在运行时覆盖。
|
配置属性 |
类型 | 默认值 |
|---|---|---|
|
是否启用过滤器。 环境变量: 更多信息 |
布尔值 |
|
|
保存CSRF令牌的表单字段名称。 环境变量: 更多信息 |
字符串 |
|
|
可提供CSRF令牌的请求头名称。 环境变量: 更多信息 |
字符串 |
|
|
CSRF Cookie名称。 环境变量: 更多信息 |
字符串 |
|
|
CSRF Cookie的最长有效时间。 环境变量: 更多信息 |
时长 |
|
|
CSRF Cookie路径。 环境变量: 更多信息 |
字符串 |
|
|
CSRF Cookie域。 环境变量: 更多信息 |
字符串 | |
|
启用后,即使使用HTTP,CSRF Cookie的secure属性也设为true。在终止SSL的反向代理后面运行时,可能需要此设置。使用HTTPS时,无论该配置是否为false,Cookie始终标记为secure。 环境变量: 更多信息 |
布尔值 |
|
|
设置HttpOnly,防止JavaScript访问Cookie。 环境变量: 更多信息 |
布尔值 |
|
|
仅当HTTP GET的相对请求路径匹配此属性配置的某个路径时,创建CSRF令牌。多个路径用逗号分隔。 环境变量: 更多信息 |
字符串列表 |
|
|
随机CSRF令牌的字节数。 环境变量: 更多信息 |
int |
|
|
CSRF令牌的HMAC签名密钥。设置时必须至少包含32个字符。 环境变量: 更多信息 |
字符串 | |
|
是否由CSRF过滤器验证令牌。也可以禁用此属性,在应用中使用JAX-RS的jakarta.ws.rs.FormParam(引用 环境变量: 更多信息 |
布尔值 |
|
|
是否要求请求体只能是application/x-www-form-urlencoded或multipart/form-data,才继续验证令牌。禁用后,其他内容类型的POST请求会跳过令牌验证。此属性仅在 环境变量: 更多信息 |
布尔值 |
|
|
Duration 格式 时长可以使用标准 也支持以数字开头的简化格式:
其他简化形式会先转换为
|
以编程方式配置 CSRF 防护
同时使用quarkus-rest-csrf与quarkus-security扩展时,可以通过io.quarkus.vertx.http.security.HttpSecurity CDI事件,以编程方式定制CSRF防护:
package org.acme.http.security;
import io.quarkus.vertx.http.security.CSRF;
import io.quarkus.vertx.http.security.HttpSecurity;
import jakarta.enterprise.event.Observes;
public class CsrfProgrammaticConfig {
void configure(@Observes HttpSecurity httpSecurity) {
httpSecurity.csrf(CSRF.builder() (1)
.formFieldName("my-csrf-token")
.tokenSize(32)
.build());
}
}
| 1 | 创建quarkus-rest-csrf扩展提供的CSRF防护配置构建器。如果缺少该扩展,调用CSRF.builder()会失败。 |
参考资料
-
OWASP跨站请求伪造
-
Qute参考手册
-
跨源资源共享
-
Quarkus安全概览











暂无评论内容