====== Apricot CSRF対策 ====== --- //[[http://www.y2sunlight.com|y2sunlight]] 2020-05-15// [[apricot:top|Apricot に戻る]] 関連記事 * [[apricot:configuration|Apricot プロジェクトの作成]] * [[apricot:public|Apricot 公開フォルダ]] * [[apricot:core:top|Apricot コア]] * [[apricot:app:top|Apricot アプリ]] * Apricot 拡張 * [[apricot:ext:middleware|Apricot ミドルウェア]] * [[apricot:ext:access-log|Apricot アクセスログ]] * Apricot CSRF対策 * [[apricot:ext:user-auth|Apricot ユーザ認証]] * [[apricot:ext:basic-auth|Apricot 基本認証]] * [[apricot:ext:session-auth|Apricot セッション認証]] * [[apricot:ext:interceptor|Apricot インターセプター]] * [[apricot:ext:di-container|Apricot DIコンテナー]] ミドルウェアを使ってCSRF対策を行います。CSRFとは Cross-Site Request Forgeries ( [[https://ja.wikipedia.org/wiki/%E3%82%AF%E3%83%AD%E3%82%B9%E3%82%B5%E3%82%A4%E3%83%88%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%83%95%E3%82%A9%E3%83%BC%E3%82%B8%E3%82%A7%E3%83%AA|クロスサイトリクエストフォージェリ]] ) の略で、Webアプリケーションの脆弱性を利用した攻撃の一種です。 CSRF対策されいない場合、悪意のある他サイトの偽装フォームから、現在ログイン中のアプリにリクエストが送信されてしまいます。例えばApricotの例でいうと、Apricotにログイン中の時、他サイトの偽装フォームからApricotのユーザ登録リクエストが受け付けられてしまいます。ほとんどのブラウザではすべての画面でセッション状態を共有するという仕様があるので発生する問題ですが、この仕様はその利便性の為に排除できるものではありません。この為に、アプリではCSRF対策が必須となります。単純な対処方としては、リクエストの送信元URLや送信元IPを調べれば良いのですが、これも偽装が可能な為、厳格に対処するには、フォームに第三者が知りえないランダムなトークンを埋め込んでそれを比較する方法が一般的に用いられています。 ---- ===== CsrfTokenクラス ===== まず、AprocotのコアにCSRF対策で使用するユーティリティクラス( CsrfToken )を作ります。CsrfTokenは以下の公開メソッドを持ちます(全てstatic)。 ^メソッド^機能^ |static\\ verify():bool|フォームの送信データ(Input)とセッション(Session)に格納されているCSRFトークンを比較して同じならtrueを返します。この機能はHTTPメソッドがPOSTの場合のみ有効です。GETの場合は常にtrueを返します。\\ \\ このメソッドはアプリケーション設定([[#CSRF対策の設定|csrf.disposable]])がtrueの場合、セッション内のCSRFトークンを削除します。| |static\\ generate()|セッション内のCSRFトークンが未生成の場合、生成してセッションに格納します。| これらのメソッドは[[#ミドルウェア|ミドルウェア]]で使用することを想定しています。HTMLに埋め込むCSRFトークンはセッションから取得して下さい(後述の[[#BladeOneのカスタマイズ|BladeOneのカスタマイズ]]を参照)。 CsrfTokenクラスを以下に示します。 {{fa>folder-open-o}} ** /apricot/core/Foundation/Security ** \\ ===== ミドルウェア ===== 以下に、CSRF対策の実装を示します。 {{fa>folder-open-o}} ** /apricot/app/Middleware ** exclude)) { // CSRFトークンの検証を行う if (!CsrfToken::verify()) { throw new \Core\Exceptions\TokenMismatchException('VerifyCsrfToken Error'); } } // CSRFトークンを生成する CsrfToken::generate(); return $next->invoke(); } } * [[#Middleware インターフェース|Middleware インターフェース]]の process() メソッドを実装します。 * $this->exclude 配列に含まれているコントローラは認証から除外します。(公開のWebAPIなど) * CsrfToken::verify() メソッドでCSRFトークンの検証を行います。 * 失敗の場合 * TokenMismatchException 例外をスローします。\\ この例外は[[apricot:app:error#集約例外コントローラ|集約例外コントローラ]]でHTTPステータスコード 419 として処理されます。 * 成功の場合 * CsrfToken::generate() で CSRFトークンの生成を試みます。 * 前処理の後、次の Invoker の invoke() メソッドを呼び出します。 \\ ===== CSRF対策の設定 ===== CSRF対策の設定は、アプリケーションの設定ファイル(app.php)で行います。 {{fa>folder-open-o}} ** /apricot/config** [ ... ], 'middleware' =>[ \App\Middleware\AccessLog::class, /* Access log */ \App\Middleware\VerifyCsrfToken::class, /* Verify CSRF Token */ ], 'csrf' =>[ 'disposable' => false, ], ]; * middleware にミドルウェア \App\Middleware\Auth\VerifyCsrfToken::class を追加します。 * csrf.disposable はCSRFトークンを使い捨てするか否かの設定を行います(既定値: false)。 * true --- 使い捨てにする(リクエスト毎に発行) * false --- 使い捨てにしない(セッション内で同じ) \\ ===== BladeOneのカスタマイズ ===== テンプレートエンジンである[[basic-library:bladeone:3.37|BladeOne]]の初期設定ファイル( bladeone.setup.php )を変更して、@csrf ディレクティブの実装を追加します。@csrf は CSRF対策を行う全てのHTMLフォームタグ(form)の中で使用します。 {{fa>folder-open-o}} ** /apricot/config/setup ** "; }); // @csrf directive \Core\View::directive('csrf', function() { $name = \Core\Foundation\Security\CsrfToken::CSRF_KEY; return ''; }); return true; // Must return true on success }; * @now ディレクティブの後に、@csrf ディレクティブの実装を追加します。 * @csrf ディレクティブでは、ミドルウェアで生成したセッション内のCSRFトークンをInput要素(type="hidden")に保存します。 \\ ===== HTMLテンプレートの修正 ===== これまでに作成したフォーム画面のHTMLテンプレートを修正し、@csrfディレクティブを追加します。 ==== ユーザ登録画面 ==== ユーザ登録の新規登録画面と編集画面のテンプレートを修正します。 === 新規登録画面のテンプレート === {{fa>folder-open-o}} ** /apricot/assets/views/user ** {{-- コンテンツ --}} @section('content')
@csrf ...
@endsection
* ''form'' タグの下に ''@csrf'' を追加します。 === 編集画面のテンプレート === {{fa>folder-open-o}} ** /apricot/assets/views/user ** {{-- コンテンツ --}} @section('content')
@csrf ...
@endsection
* ''form'' タグの下に ''@csrf'' を追加します。 \\ ===== テスト実行 ===== CSRF対策のテストをしてみましょう。Apricotのユーザ編集画面にアクセスします。 [{{apricot:ext:ext01.png?nolink}}] ■ [保存]ボタンを押して下さい。 [{{apricot:ext:ext02.png?nolink}}] ■ 正常に保存できます。\\ ■ 先に修正したユーザ編集画面の ''@csrf'' ディレクティブをコメントにして下さい。
{{fa>folder-open-o}} ** /apricot/assets/views/user ** ....
{{--@csrf --}} ...
....
■ ユーザ編集画面で再び[保存]ボタンを押すと次の画面が出ます。 [{{apricot:ext:ext03.png?nolink}}] ■ ''VerifyCsrfToken Error'' のメッセージと共にWhoopsのエラー画面が出力されます。\\ ■ これは CSRFトークンがサーバーに送信されていないので発生する例外です。\\ ■ 尚、本番用のエラー画面を出力したい場合は、.env の ''APP_DEBUG'' をfalseに設定してからテストして下さい。
.... APP_DEBUG=false ....
テスト後は、''@csrf'' や ''APP_DEBUG'' を元に戻しておいて下さい。 \\