Chapter 07

SpringSecurityの導入/CSRF対策

あしたば
あしたば
2021.03.21に更新

この章で学ぶこと

とりあえずアプリを動かすことができましたが、今のアプリケーションは大変にセキュリティが脆弱です。
このままでは例の事件のような犠牲者が出てしまうかもしれません。

開発者が気をつけるべきセキュリティは非常に多岐にわたり、それだけで専門の職があったり資格があったりと言う需要があります。
そのすべてを今学ぶことは不可能ですが、身近な脆弱性としてCSRF、クロスサイトリクエストフォージェリについて体験し、対策をとってみましょう。

ちなみに、より良いWebアプリケーションの技術者になるために、体系的に安全なWebアプリケーション開発を学ぼうというのであれば、はじめの一歩になる選択肢はほぼ一つです。
徳丸本を読んでみましょう。これにつきます。

  • CSRF(クロスサイトリクエストフォージェリ)とはなんなのか
  • SpringBootでの対策

CSRF(クロスサイトリクエストフォージェリ)とはなんなのか

CSRF(しーさーふ)、Cross-Site Request Forgeriesは、あるWebサービス利用者に対して、そのWebサービス上の取り消しのきかない処理を、全く別のWebサイト上等から勝手に実行させ"られる"ことを可能にする脆弱性を指します。
この脆弱性を突いた攻撃をそのままCSRF攻撃と呼びます。

殆どの場合、CSRF攻撃が問題になるのはログイン処理のあるWebサービスであることが多く、「利用者の意図したリクエスト」かどうかを確認しないために発生する脆弱性です。
例えば、

  • 買う気のない商品を購入させられる / カートに商品を入れるリクエストと、購入のリクエストを勝手に実行させられたから
  • 勝手にサービスを退会させられる / 退会のリクエストを勝手に送られたから
  • 勝手にパスワードを変更させられる / パスワードの変更リクエストを勝手に送られたから

といったような例があります。

例外的に、ログイン処理のないサービスでもCSRFが問題になるケースがあります。
その実例がまさに掲示板です。
もし、掲示板がCSRF脆弱性をもっていると、ある罠サイトを見た段階で、有名掲示板に犯罪予告を送信させられるでしょう。この場合、掲示板側に残るのは間違いなくあなたのIPアドレスになるため、疑いを晴らす事に苦労するはずです。

なぜ、意図せずリクエストをおくられているのに、自分自身のIPアドレスが残ってしまうのでしょうか。
われわれの作った掲示板にはまさにCSRF脆弱性がありますから、それを試してみましょう。

CSRFを体験する

下記のようなhtmlを用意してください。
これはアプリケーションには必要ないので、捨てる前提でどこにつくってもよいです。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form method="post" action="http://localhost:8080/board">
        <input type="hidden" name="name" value="ななしさん">
        <input type="hidden" name="mailAddress" value="example1@example.com">
        <input type="hidden" name="comment" value="2021/4/20にわたしはカレールーとライスを1:1で皿に配置した上でグチャグチャにかき混ぜてやります">
        <button>5000兆円もらえるボタン</button>
    </form>
</body>
</html>

そして、掲示板アプリケーションを実行し、画面を開いておきましょう。

先程用意したHTMLをブラウザで開いてみましょう。

このように、魅惑のボタンだけがあるシンプルなページです。
思わず押したくなりますね。

押すと、なんとういうことでしょう。

凶悪な犯罪予告が書き込まれてしまいました。これでは逮捕されてしまいます。
一体何が起こったのでしょうか。

フォームサブミットの罠

先程の通信内容を検証ツールで覗いてみましょう。

すると、特になんのひねりもなく、データが送信されてしまっていることがわかります。
GeneralにあるRequesrURLは掲示板です。

問題なのはFormDataで、これは書いた覚えがありませんね。
これは当然で、罠HTMLのフォームに埋め込まれているからです。

    <form method="post" action="http://localhost:8080/board">
        <input type="hidden" name="name" value="ななしさん">
        <input type="hidden" name="mailAddress" value="example1@example.com">
        <input type="hidden" name="comment" value="2021/4/20にわたしはカレールーとライスを1:1で皿に配置した上でグチャグチャにかき混ぜてやります">
        <button>5000兆円もらえるボタン</button>
    </form>

このように、formのactionを悪用すると、別のWebサイトのURLに対してPOSTメソッドが実行可能です。
そして、これは利用者のパソコンの上で実行されますので、そのブラウザに認証情報が残っていればそのままログイン認証を突破することもありえます。

前段に書いた、「利用者の意図したリクエスト」かどうかを確認、とは、まさにそのままの意味で、このように身に覚えのないリクエストを防止するためにはユーザが意図して送ってきているのかを検証しなければなりません。

SpringBootでの対策

幸いにも、SpringにはSpringSecurityという心強いセキュリティのためのフレームワークが存在します。
(入れたら安全というわけではないですが)

では、pom.xmlのdependenciesに下記を追加してください。

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

そして、configパッケージを作成し、SecurityConfigクラスを追加しましょう。

内容は下記です。

package chalkboard.me.bulletinboard.config;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  @Override
  public void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().anyRequest().permitAll();
  }
}

これにより、SpringSecurityのデフォルトのセキュリティ機能が有効化されます。
※ベーシック認証を意図的に無効化しています

CSRF対策

さて、CSRFの対策を行ってみましょう。

実は、現時点でSpringSecurityのCSRF対策は組み込み済みで、あとはそれを有効化するのみです。
thymeleafがSpringSecurityの機能を使えるように、board.htmlのフォームに少し手を加えましょう。

<form method="POST" th:action="@{/board}" th:object="${commentForm}">

actionの記載を変えたのみですが、これで終了です。

もう一度アプリケーションを実行し、あの問題のHTMLからCSRF攻撃を試みてみましょう。
するとどうでしょうか、

HttpStatus 403, Forbidden が返却されています。
これはアクセスする権限が貴様にない、ということを示しており、SpringSecurityが働いた結果です。
一体、どのようにして 「利用者の意図したリクエスト」かどうかを確認したのでしょうか。

掲示板アプリケーションが生成したHTMLを確認してみましょう。

すると、_csrfという見に覚えのないhiddenパラメータが存在することがわかります。
これを使ってSpringSecurityは 「利用者の意図したリクエスト」かどうかを確認 しているのですが、その理解にはいくつかの知識が必要です。

CSRFトークン

では、一体どのようにしてSpringSecurityは 「利用者の意図したリクエスト」かどうかを確認 したのか、結論から述べましょう。
その後、各種知識について簡単に紹介し、理解のためのリファレンスを示します。

SpringSecurityが標準で提供するCSRFへの対策は、Synchronizer Token Patternと呼ばれます。
この方法はセッションCookieと、リクエストごとに発行されるランダムなトークンを要求し、受け取ったトークンを検証することでリクエストを保証します。

段階ごとに説明します。

  1. ユーザが、重要なリクエストをする前段階の画面にアクセスします。このアクセスはほとんどのケースでGETメソッドによるアクセスです。(今回の場合、掲示板を表示するだけの処理)
    GETアクセスが発生したら、SpringBootはユーザのセッションを作成し、CSRFトークンと呼ばれるランダムな文字列を生成します。セッションにCSRFトークンを保存し、セッションの手がかり(SessionID)をCookieにのせてGETアクセスのレスポンスで返します。また、HTMLのFormにCSRFトークンをレンダリングしておきます。
  2. 重要なリクエストはほとんどのケースでPOSTで行われます。このとき、ブラウザはそのWebサイトから貰ったCookieと、Formにあるデータを送信します。
  3. SpringSecurityは、リクエストされたCookieからセッションを特定し、そのセッションに保存されているCSRFトークンと、リクエストされたCSRFトークンを比較します。これで一致すればそのリクエストを受入れ、一致しなかったり不足があれば403エラーとして拒否します。

実際に画面上で追ってみましょう。

1.ユーザのGETアクセス

ユーザが掲示板にアクセスした段階で、

  • セッションが作成される
  • セッションにはCSRFトークンが保存される
  • Formタグ内にCSRFトークンがレンダリングされる
  • セッションIDを示すCookieが発行される

が発生します。実際に検証ツールでCookieを確認してみましょう。
applicationタブでCookieを見ることができます。

JSESSIONID なるCookieが発行されていることがわかります。このCookieが、セッションIDをもっています。
Cookieはブラウザごとにもっていますから、アクセスする人が違えば当然JSESSIONIDが示すセッションも異なります。

セッションそのものはWebサーバ側に存在するため見ることができませんが、そのセッションにはCSRFトークンが作られています。
値は、今表示されているものと同じです。

2.フォーム送信

さて、適当に掲示板に入力し、送信してみましょう。
その送信データをブラウザの検証ツールで確認してみます。
ポイントはリクエストヘッダとフォームデータです。


リクエストヘッダには送信されたCookieが含まれます。見ての通り、JSESSIONIDが送信されていますね。
SpringBootはこのJSESSIONIDをもとに検証すべき正解のCSRFトークンが入っているセッションを特定します。


フォームデータには_csrfがありますね。これがCSRFトークンです。
このトークンがセッションに入っているCSRFトークンと一致するかを確認しています。

理解のポイント

少し難しかったと思いますが、雰囲気はつかめたでしょうか。
この説明において、全く解説していない概念は色々ありますが、2つ最低限知ってほしいことがあります。
Cookieセッションです。
どちらもWebアプリケーションの開発に仕事として携わるのなら必修の概念なので、知らなければWebを支える技術などを読んでみてください。

ここでは、簡単な説明に留めます。

cookieはブラウザに保存される小さなデータです。WebサーバのレスポンスにSet-Cookieヘッダーが含まれると、ブラウザが保存しておいてくれています。
その用途はセッション向けのハッシュ化されたIDユーザ追跡目的のIDログイン中の正当性検証用のパラメータなど多岐にわたります。
というか、4,096バイトのデータ量に収まっているなら、どんなデータも入れられます。なので、用途は色々あります

Cookieは改ざん可能かつ、盗聴リスクがありますのでPW/IDのようなセキュア情報、個人情報を持ってはいけません。JSのWebStorageも同じです

Cookieには名前と値の他に、様々なオプションがあります。今回重要になるのはDomain設定です。

CookieのDomainは、そのCookieが有効なWebサイトを示します。
今回の場合はlocalhostとなっています。

CSRFは殆どの場合、ターゲットとなるWebサイトとは別の罠サイトを作って攻撃を行いますので、ドメインが異なっているのです。
そのため、Cookieのドメインが一致しないWebサイトからは、そもそもセッションIDを送ることが難しくなります。

セッション

セッションという単語自体相当に意味が多く、調べづらい単語になります。
今回の文脈ではセッション管理 と検索するのがおすすめです。

情報をクライアント だけ に持つか、サーバに持つかは実装によって異なります。
共通なのは、セッションIDを頼りにして、複数いる利用ユーザそれぞれの状態を管理するものであるということです。

今回のセッションはWebサーバ側に保存できる、セッションIDごとのデータの入れ物です。
JSESSIONIDクッキーのもつセッションIDによってどのセッションを見ればよいかを特定していました。

そしてそのサーバ側のセッションのなかには正解のCSRFトークンが入っています。

この対策によって攻撃者は

  • セッションIDの特定
  • CSRFトークンの特定
  • Cookie設定の不備

などの前提を超えなければCSRF脆弱性を使った攻撃は不可能となっています。

関連リンク

セッションの説明について、下記が秀逸だなと思いました。

https://www.slideshare.net/carotene4035/sessionrailscookie-store

今回のPR

https://github.com/angelica-keiskei/spring-sample/pull/9