クロスサイトリクエストフォージェリー対策
クロスサイトリクエストフォージェリ(CSRF) とは、エンドユーザーが現在認証されているウェブアプリケーション上で意図しないアクションを実行させる攻撃です。
Quarkus Security は、 Double Submit Cookie と CSRFリクエスト・ヘッダー テクニックを実装したCSRF防止機能を提供しています。
Double Submit Cookie
テクニックを使うには、CSRF トークンを (オプションで署名された) HTTPOnly
クッキーとしてクライアントに送り、サーバサイドでレンダリングされた HTML フォームの隠されたフォーム入力に直接埋め込むか、リクエストヘッダ値として送信する必要があります。
このエクステンションは、 Quarkus REST(旧RESTEasy Reactive) サーバーフィルターで構成され、 application/x-www-form-urlencoded
および multipart/form-data
フォームで CSRF トークンの作成と検証を行います。また、Qute HTML フォームパラメータープロバイダーで、 Qute テンプレートへの CSRF トークンの注入 をサポートします。
プロジェクトの作成
まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します。
Windowsユーザーの場合:
-
cmdを使用する場合、(バックスラッシュ
\
を使用せず、すべてを同じ行に書かないでください)。 -
Powershellを使用する場合は、
-D
パラメータを二重引用符で囲んでください。例:"-DprojectArtifactId=security-csrf-prevention"
This command generates a project which imports the rest-csrf
extension.
If you already have your Quarkus project configured, you can add the rest-csrf
extension
to your project by running the following command in your project base directory:
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")
次に、このテンプレートを含むファイルを csrfToken.html
と名付け、 src/main/resources/templates
フォルダに置いてください:
<!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トークンを含むhiddenフォームフィールドを持つ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 クッキーと照合されます。 |
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
CSRF クッキーの値をヘッダとして渡すために、JavaScript から CSRF クッキーにアクセスする必要がある場合、 {inject:csrf.cookieName}
と {inject:csrf.headerName}
を使って、CSRF ヘッダ値として読まれなければならないクッキー名を注入し、このクッキーへのアクセスを許可してください:
quarkus.rest-csrf.cookie-http-only=false
クロスオリジンリソース共有
クロスオリジン環境でCSRF防止を実施したい場合は、すべてのオリジンをサポートするのは避けてください。 サポートされるOriginを信頼できるOriginのみに制限してください。詳細については、「Cross-originリソース共有」ガイドの CORSフィルター のセクションを参照してください。 |
CSRFトークンの検証を制限
Jakarta REST エンドポイントは、 application/x-www-form-urlencoded
または multipart/form-data
のペイロードを持つ HTTP POST リクエストだけでなく、他のメディアタイプを持つペイロードも、同じ URL パスまたは異なる 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 トークン検証は不要です。 |
ご覧のように、service/user
パスで application/x-www-form-urlencoded
ペイロードを受け取る際に 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 |
list of string |
|
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 |
ブーリアン |
|
期間フォーマットについて
To write duration values, use the standard 数字で始まる簡略化した書式を使うこともできます:
その他の場合は、簡略化されたフォーマットが解析のために
|