この記事は 品川 Advent Calendar 2019 という怪しげなアドベントカレンダーの3日目です。
Spring Security の CSRF 対策について、調べる機会があったのでまとめておく。
デフォルトのパターン
Spring Security がデフォルトで提供する CSRF 対策は OWASP でいうところの Synchronizer Token Pattern に該当している。
https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern
これは、サーバ側で生成したトークン値を保持しておき、クライアントが送信してきたトークン値と一致するかどうかを確認することで、対策を行っている。
サーバ側に値を保持しておく必要があるため、ステートフルな実装となる。
使い方
Spring Security の CSRF 対策はデフォルトで有効になっているため、特に設定は不要。
クライアントから CSRF トークンを送るにはリクエストヘッダにつける、パラメータに付与するのどちらかで対応できる。
ヘッダに付与する場合のヘッダ名は X-CSRF-TOKEN
で、パラメータに付与する場合のパラメータ名は _csrf
にする必要がある。
一応名前を変更することもできるが、特に変える必要性はないと思う。
クライアントから CSRF トークンを送るためにはいくつかのやり方がある。
form を使う
html の form からサブミットする場合、JSP であれば <form:form>
タグを、Thymeleaf であれば、form タグ内に th:action
をつけておけば
自動的に hidden フィールドとして CSRF トークンの値が設定される。
<form th:action="@{/action}" method="post">
....
</form>
実際には以下のような html になって出力される。
<form th:action="@{/action}" method="post">
....
<input type="hidden" name="_csrf" value="b2272ee7-fbcc-42b0-8d12-0c048a12045c" />
</form>
ちなみにこれは、CsrfRequestDataValueProcessor
というクラスによって実現されている。
手動でリクエストスコープから取り出す
JavaScript からリクエストする場合は form タグを利用しないことも多い。
CSRF トークンはリクエストスコープに格納されていて、CSRF トークンをパラメータに付与する際の名前、つまり _csrf
でアクセスできる。
例えば、Thymeleaf では以下のようにすればアクセスすることができる。(input タグである必要はない。)
<input name="${_csrf.parameterName}" type="hidden" value="${_csrf.token}"/>
実際には以下のような html になって出力される。
<input name="_csrf" type="hidden" value="b2272ee7-fbcc-42b0-8d12-0c048a12045c"/>
これを JavaScript から読み取って、リクエストすればいい。
なお、パラメータ名をデフォルトから変更している場合は、それに応じてアクセスするための名前も変更されるので注意。
CSRF トークンを返却するエンドポイントを公開する
いままでの方法では、サーバ側でレンダリングする html が必要だった。
REST API のみを提供するようなサーバでは、html を介して CSRF トークンを受け渡すことができない。
その場合、エンドポイントを公開することで対応できる。
@RestController
public CsrfController {
@GetMapping("/csrf")
public CsrfToken csrf(CsrfToken token) {
return token;
}
}
メソッドの引数に CsrfToken
を指定すると、CsrfTokenArgumentResolver
がサーバ側に保持されている値を取得して設定してくれる。
実際にリクエストしてみると以下のような JSON が返却される。
{
parameterName: "_csrf",
token: "b2272ee7-fbcc-42b0-8d12-0c048a12045c",
headerName: "X-CSRF-TOKEN"
}
ただ、このエンドポイントが他のドメインに公開されると CSRF 対策の意味がなくなるので、CORS の設定には注意すること。
Cookie を利用するパターン
いままでとは別のパターンとして、Cookie を利用して対策を行うこともできる。
これは OWASP でいうところの、Double Submit Cookie に該当している。
サーバ側は Cookie に CSRF トークンを設定する。
クライアント側は Cookie から CSRF トークンを取得して、リクエストヘッダかパラメータに設定してリクエストを送る。
サーバ側はクライアントから送信された Cookie の CSRF トークン値と、ヘッダ or パラメータの CSRF トークン値が一致しているかどうか確認することで対策を行っている。
この方法だと、サーバ側にトークンを保持しなくていいため、ステートレスな実装となる。
使い方
以下のような設定を行う。
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) {
http.csrf(csrf ->
csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
}
}
トークン値をヘッダに付与する場合のヘッダ名は X-XSRF-TOKEN
でパラメータの場合は _csrf
にする必要がある。
また、Cookie の名前は XSRF-TOKEN
となっている。
これも変更することは可能だが、変更する必要はない気がする。
クライアント側の実装
JavaScript で Cookie のから CSRF トークンの値を取得してヘッダかパラメータに設定するのみ。
AngularJS では勝手にこの処理を行ってくれるらしい。
実装例は割愛。。。
利用する場合の注意点
この方法は気軽にステートレスで実現できるが、いくつか CSRF 対策できないシナリオが存在する。
徳丸先生の以下の記事で具体的なシナリオが記載されている。(解答2の部分)
OWASP の Double Submit Cookie に関する解説内にも以下の記述がある。
This technique works as long as you are sure that your subdomains are fully secured and only accept HTTPS connections
詳細は OWASP のページを参照。
Double Submit Cookie による対策を行う場合には、上記のような条件が許容できるかどうかを判断する必要がある。
さいごに
最近のフレームワークは賢くて開発者が意識せずとも色んな脆弱性の対策してくれるようになっているが、それは開発者が脆弱性について理解していなくてもいい、というわけではない。(戒め)
ちゃんと勉強した上で、実装はフレームワークにお任せというスタンスがいいんだろうと思う。(戒め)
OWASP Cheat Sheet Series の日本語訳ページがほしい。。。