Spring Security の CSRF 対策を学ぼう

この記事は 品川 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 トークンの値が設定される。

```html
<form th:action="@{/action}" method="post">
  ....
</form>
```

実際には以下のような html になって出力される。

```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 タグである必要はない。）

```html
<input name="${_csrf.parameterName}" type="hidden" value="${_csrf.token}"/>
```

実際には以下のような html になって出力される。

```html
<input name="_csrf" type="hidden" value="b2272ee7-fbcc-42b0-8d12-0c048a12045c"/>
```

これを JavaScript から読み取って、リクエストすればいい。
なお、パラメータ名をデフォルトから変更している場合は、それに応じてアクセスするための名前も変更されるので注意。

### CSRF トークンを返却するエンドポイントを公開する

いままでの方法では、サーバ側でレンダリングする html が必要だった。
REST API のみを提供するようなサーバでは、html を介して CSRF トークンを受け渡すことができない。
その場合、エンドポイントを公開することで対応できる。

```java
@RestController
public CsrfController {
    @GetMapping("/csrf")
    public CsrfToken csrf(CsrfToken token) {
        return token;
    }
}
```

メソッドの引数に `CsrfToken` を指定すると、`CsrfTokenArgumentResolver` がサーバ側に保持されている値を取得して設定してくれる。
実際にリクエストしてみると以下のような JSON が返却される。

```javascript
{
  parameterName: "_csrf",
  token: "b2272ee7-fbcc-42b0-8d12-0c048a12045c",
  headerName: "X-CSRF-TOKEN"
}
```

ただ、このエンドポイントが他のドメインに公開されると CSRF 対策の意味がなくなるので、CORS の設定には注意すること。

# Cookie を利用するパターン

いままでとは別のパターンとして、Cookie を利用して対策を行うこともできる。
これは OWASP でいうところの、**Double Submit Cookie** に該当している。

https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie

サーバ側は Cookie に CSRF トークンを設定する。
クライアント側は Cookie から CSRF トークンを取得して、リクエストヘッダかパラメータに設定してリクエストを送る。
サーバ側はクライアントから送信された Cookie の CSRF トークン値と、ヘッダ or パラメータの CSRF トークン値が一致しているかどうか確認することで対策を行っている。

この方法だと、サーバ側にトークンを保持しなくていいため、ステートレスな実装となる。

## 使い方

以下のような設定を行う。

```java
@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の部分）

https://blog.tokumaru.org/2018/11/csrf_26.html

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 の日本語訳ページがほしい。。。