防范跨站请求伪造

防范跨站请求伪造

跨站请求伪造(CSRF)会诱使用户在已经登录的Web应用中执行并非其本意的操作。

Quarkus Security通过双重提交Cookie与CSRF请求头技术提供CSRF防护。

双重提交Cookie要求把CSRF令牌作为HttpOnly Cookie发送给客户端,Cookie可选择签名;同时把令牌直接嵌入服务端渲染HTML表单的隐藏字段,或者作为请求头值提交。

如果需要不由服务器创建Cookie的无状态CSRF防护,可查看CORS过滤器。尽管名为CORS,它也会检查请求Origin是否匹配目标Host,或是否位于服务器允许的Origin列表中,从而提供CSRF防护。

该扩展包含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

配置参考

带标记的配置在构建时固定;其余配置可以在运行时覆盖。

配置属性

类型 默认值

quarkus.rest-csrf.enabled

是否启用过滤器。

环境变量: QUARKUS_REST_CSRF_ENABLED

更多信息

布尔值

true

quarkus.rest-csrf.form-field-name

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

环境变量: QUARKUS_REST_CSRF_FORM_FIELD_NAME

更多信息

字符串

csrf-token

quarkus.rest-csrf.token-header-name

可提供CSRF令牌的请求头名称。

环境变量: QUARKUS_REST_CSRF_TOKEN_HEADER_NAME

更多信息

字符串

X-CSRF-TOKEN

quarkus.rest-csrf.cookie-name

CSRF Cookie名称。

环境变量: QUARKUS_REST_CSRF_COOKIE_NAME

更多信息

字符串

csrf-token

quarkus.rest-csrf.cookie-max-age

CSRF Cookie的最长有效时间。

环境变量: QUARKUS_REST_CSRF_COOKIE_MAX_AGE

更多信息

时长

2H

quarkus.rest-csrf.cookie-path

CSRF Cookie路径。

环境变量: QUARKUS_REST_CSRF_COOKIE_PATH

更多信息

字符串

/

quarkus.rest-csrf.cookie-domain

CSRF Cookie域。

环境变量: QUARKUS_REST_CSRF_COOKIE_DOMAIN

更多信息

字符串

quarkus.rest-csrf.cookie-force-secure

启用后,即使使用HTTP,CSRF Cookie的secure属性也设为true。在终止SSL的反向代理后面运行时,可能需要此设置。使用HTTPS时,无论该配置是否为false,Cookie始终标记为secure。

环境变量: QUARKUS_REST_CSRF_COOKIE_FORCE_SECURE

更多信息

布尔值

false

quarkus.rest-csrf.cookie-http-only

设置HttpOnly,防止JavaScript访问Cookie。

环境变量: QUARKUS_REST_CSRF_COOKIE_HTTP_ONLY

更多信息

布尔值

true

quarkus.rest-csrf.create-token-path

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

环境变量: QUARKUS_REST_CSRF_CREATE_TOKEN_PATH

更多信息

字符串列表

quarkus.rest-csrf.token-size

随机CSRF令牌的字节数。

环境变量: QUARKUS_REST_CSRF_TOKEN_SIZE

更多信息

int

16

quarkus.rest-csrf.token-signature-key

CSRF令牌的HMAC签名密钥。设置时必须至少包含32个字符。

环境变量: QUARKUS_REST_CSRF_TOKEN_SIGNATURE_KEY

更多信息

字符串

quarkus.rest-csrf.verify-token

是否由CSRF过滤器验证令牌。也可以禁用此属性,在应用中使用JAX-RS的jakarta.ws.rs.FormParam(引用form-field-name字段)和jakarta.ws.rs.CookieParam(引用RestCsrfConfig#cookieName Cookie)自行比较。即使关闭过滤器的令牌验证,它仍会检查令牌存在、字节数符合token-size,以及Content-Type为application/x-www-form-urlencoded或multipart/form-data。

环境变量: QUARKUS_REST_CSRF_VERIFY_TOKEN

更多信息

布尔值

true

quarkus.rest-csrf.require-form-url-encoded

是否要求请求体只能是application/x-www-form-urlencoded或multipart/form-data,才继续验证令牌。禁用后,其他内容类型的POST请求会跳过令牌验证。此属性仅在verify-token启用且未配置token-header-name时生效。

环境变量: QUARKUS_REST_CSRF_REQUIRE_FORM_URL_ENCODED

更多信息

布尔值

true

Duration 格式

时长可以使用标准java.time.Duration格式。详情参见Duration#parse()的Java API文档。

也支持以数字开头的简化格式:

  • 只有数字时,单位为秒。

  • 数字后跟ms时,单位为毫秒。

其他简化形式会先转换为java.time.Duration格式再解析:

  • 数字后跟h、m或s时,添加PT前缀。

  • 数字后跟d时,添加P前缀。

以编程方式配置 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跨站请求伪造

  • Quarkus REST

  • Qute参考手册

  • 跨源资源共享

  • Quarkus安全概览

正文相关链接

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容