クロスサイトリクエストフォージェリー対策
クロスサイトリクエストフォージェリ (CSRF) は、エンドユーザーが現在認証されている Web アプリケーション上で、望まないアクションを強制的に実行させる攻撃です。
Quarkus Security は、 Double Submit Cookie と CSRF リクエストヘッダー テクニックを実装した CSRF 防止機能を提供します。
Double Submit Cookie テクニックでは、CSRF トークンを HTTPOnly (オプションで署名された) クッキーとしてクライアントに送信し、サーバーサイドでレンダリングされる HTML フォームの隠しフォーム入力に直接埋め込むか、リクエストヘッダー値として送信する必要があります。
|
サーバーがクッキーを作成しないステートレスな CSRF 防止策をお探しの場合は、 CORS フィルター をご覧ください。その名前にもかかわらず、リクエストの |
このエクステンションは、 application/x-www-form-urlencoded および multipart/form-data フォームで CSRF トークンを作成および検証する Quarkus REST (旧 RESTEasy Reactive) サーバーフィルターと、 Qute テンプレートへの CSRF トークンの注入 をサポートする Qute HTML フォームパラメータープロバイダーで構成されます。
CSRF 防止フィルターは、HTTP POST、PUT、PATCH、DELETE、および REST アプリケーションの状態を変更できるその他のメソッドを使用するリクエストに適用されます。
プロジェクトの作成
まず、新しいプロジェクトが必要です。以下のコマンドで新しいプロジェクトを作成します。
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 クッキーと照合して検証されます。 |
次に、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 クッキーの欠落、または CSRF フォームフィールドと CSRF クッキーの値が一致しないことを検出した場合、フォームの POST リクエストは HTTP ステータス 400 で失敗します。
この段階では、追加の設定は必要ありません。デフォルトでは、CSRF フォームフィールドとクッキー名は csrf-token に設定され、フィルターがトークンを検証します。ただし、必要に応じてこれらの名前を変更できます。
quarkus.rest-csrf.form-field-name=csrftoken
quarkus.rest-csrf.cookie-name=csrftoken
CSRF トークンに署名
生成された CSRF トークンに対して HMAC 署名を作成し、これらの HMAC 値を CSRF トークンクッキーとして保存することで、攻撃者が CSRF クッキートークンを再作成するリスクを回避できます。必要なのは、少なくとも 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 クッキーと照合して検証されます。 |
デフォルトのヘッダー名は X-CSRF-TOKEN です。 quarkus.rest-csrf.token-header-name でカスタマイズできます (例:)
quarkus.rest-csrf.token-header-name=CUSTOM-X-CSRF-TOKEN
JavaScript から CSRF クッキーにアクセスしてその値をヘッダーとして渡す必要がある場合は、 {inject:csrf.cookieName} と {inject:csrf.headerName} を使用して、CSRF ヘッダー値として読み取られる必要があるクッキー名を注入し、このクッキーへのアクセスを許可します。
quarkus.rest-csrf.cookie-http-only=false
クロスオリジンリソース共有
|
クロスオリジン環境で CSRF 防止を実施したい場合は、すべての Origin をサポートすることは避けてください。 サポートされる Origin を信頼できる Origin のみに制限してください。詳細については、「Cross-origin resource sharing」ガイドの 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 | /user への POST フォームリクエストでは、CSRF フィルターによって CSRF トークン検証が適用されます。 |
| 2 | /user への POST JSON リクエストでは、CSRF トークン検証は不要です。 |
| 3 | /users への POST JSON リクエストでは、CSRF トークン検証は不要です。 |
ご覧のように、 application/x-www-form-urlencoded ペイロードを受け入れる /service/user パスでは CSRF トークンの検証が必要ですが、 /service/user と /service/users の両方のメソッドに POST される 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 フォームフィールドとクッキーの値を比較したい場合は、次のようにします。
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 フォームフィールドとクッキーの値を比較し、一致しない場合は HTTP ステータス 400 で失敗します。 |
また、フィルターでトークンの検証を無効にします。
quarkus.rest-csrf.verify-token=false
設定リファレンス
ビルド時に固定される設定プロパティー - 他のすべての設定プロパティーは実行時にオーバーライド可能
Configuration property |
タイプ |
デフォルト |
|---|---|---|
If filter is enabled. Environment variable: Show more |
ブーリアン |
|
Form field name which keeps a CSRF token. Environment variable: Show more |
string |
|
Token header which can provide a CSRF token. Environment variable: Show more |
string |
|
CSRF cookie name. Environment variable: Show more |
string |
|
CSRF cookie max age. Environment variable: Show more |
|
|
CSRF cookie path. Environment variable: Show more |
string |
|
CSRF cookie domain. Environment variable: Show more |
string |
|
If enabled the CSRF cookie will have its 'secure' parameter set to 'true' when HTTP is used. It may be necessary when running behind an SSL terminating reverse proxy. The cookie will always be secure if HTTPS is used even if this property is set to false. Environment variable: Show more |
ブーリアン |
|
Set the HttpOnly attribute to prevent access to the cookie via JavaScript. Environment variable: Show more |
ブーリアン |
|
Create CSRF token only if the HTTP GET relative request path matches one of the paths configured with this property. Use a comma to separate multiple path values. Environment variable: Show more |
文字列のリスト |
|
Random CSRF token size in bytes. Environment variable: Show more |
int |
|
CSRF token HMAC signature key, if this key is set then it must be at least 32 characters long. Environment variable: Show more |
string |
|
Verify CSRF token in the CSRF filter. If you prefer then you can disable this property and compare CSRF form and cookie parameters in the application code using JAX-RS jakarta.ws.rs.FormParam which refers to the Environment variable: Show more |
ブーリアン |
|
Require that only 'application/x-www-form-urlencoded' or 'multipart/form-data' body is accepted for the token verification to proceed. Disable this property for the CSRF filter to avoid verifying the token for POST requests with other content types. This property is only effective if Environment variable: Show more |
ブーリアン |
|
|
期間フォーマットについて
期間の値を書くには、標準の 数字で始まる簡略化した書式を使うこともできます:
その他の場合は、簡略化されたフォーマットが解析のために
|
プログラムによる 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() の呼び出しは失敗します。 |