メインメニュー
XAMPP アレンジ
IED
WSL2
-
道具箱
リポジトリ編
フレームワーク編
公開ソフトウェア
メタ
リンク
- PHP ライブラリ
- PHP 言語
apricot:ext:csrfApricot CSRF対策
— y2sunlight 2020-05-15
関連記事
- Apricot 拡張
- Apricot CSRF対策
ミドルウェアを使ってCSRF対策を行います。CSRFとは Cross-Site Request Forgeries ( クロスサイトリクエストフォージェリ ) の略で、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.disposable)がtrueの場合、セッション内のCSRFトークンを削除します。static
generate()セッション内のCSRFトークンが未生成の場合、生成してセッションに格納します。 これらのメソッドはミドルウェアで使用することを想定しています。HTMLに埋め込むCSRFトークンはセッションから取得して下さい(後述のBladeOneのカスタマイズを参照)。
CsrfTokenクラスを以下に示します。
/apricot/core/Foundation/Security
- CsrfToken.php
<?php namespace Core\Foundation\Security; use Core\Input; use Core\Session; /** * CSRF */ class CsrfToken { /** * @var integer */ private const CSRF_LENGTH = 40; /** * Session/Post key for CSRF */ public const CSRF_KEY = '_token'; /** * verify CSRF Token * @return bool Returns true on success */ public static function verify():bool { $ret = true; // Verify CSRF token in case of POST method if (strtoupper($_SERVER['REQUEST_METHOD'])=='POST') { if (Input::get(self::CSRF_KEY,'A') != Session::get(self::CSRF_KEY,'B')) { \Core\Log::error('VerifyCsrf Error',[Input::get(self::CSRF_KEY),Session::get(self::CSRF_KEY)]); $ret = false; } } // Delete CSRF token of Input Input::remove(self::CSRF_KEY); // If the CSRF token is disposable, delete it from the session if (app('csrf.disposable',false)) { Session::remove(self::CSRF_KEY); } return $ret; } /** * Generate new CSRF Token in Session * @return bool */ public static function generate() { // Generate a CSRF token if it has not been generated if (!Session::has(self::CSRF_KEY) || empty(Session::get(self::CSRF_KEY))) { // Generate a new CSRF token for the next request Session::set(self::CSRF_KEY, str_random(self::CSRF_LENGTH)); } } }
ミドルウェア
以下に、CSRF対策の実装を示します。
/apricot/app/Middleware
- VerifyCsrfToken.php
<?php namespace App\Middleware; use Core\Foundation\Response; use Core\Foundation\Invoker; use Core\Foundation\Middleware\Middleware; use Core\Foundation\Security\CsrfToken; /** * CSRFトークンの検証 - Middleware */ class VerifyCsrfToken implements Middleware { /** * Excludeing controller * @var array */ private $exclude = [ 'HogeHogeController', // For example: Web API controller etc. ]; /** * Process incoming requests and produces a response * {@inheritDoc} * @see \Core\Foundation\Middleware\Middleware::invoke() */ public function process(Invoker $next): Response { if (!in_array(controllerName(), $this->exclude)) { // CSRFトークンの検証を行う if (!CsrfToken::verify()) { throw new \Core\Exceptions\TokenMismatchException('VerifyCsrfToken Error'); } } // CSRFトークンを生成する CsrfToken::generate(); return $next->invoke(); } }
- Middleware インターフェースの process() メソッドを実装します。
- $this->exclude 配列に含まれているコントローラは認証から除外します。(公開のWebAPIなど)
- CsrfToken::verify() メソッドでCSRFトークンの検証を行います。
- 失敗の場合
- TokenMismatchException 例外をスローします。
この例外は集約例外コントローラでHTTPステータスコード 419 として処理されます。
成功の場合- CsrfToken::generate() で CSRFトークンの生成を試みます。
- 前処理の後、次の Invoker の invoke() メソッドを呼び出します。
CSRF対策の設定
CSRF対策の設定は、アプリケーションの設定ファイル(app.php)で行います。
/apricot/config
- app.php
<?php return [ 'setup' =>[ ... ], '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のカスタマイズ
テンプレートエンジンであるBladeOneの初期設定ファイル( bladeone.setup.php )を変更して、@csrf ディレクティブの実装を追加します。@csrf は CSRF対策を行う全てのHTMLフォームタグ(form)の中で使用します。
/apricot/config/setup
- bladeone.setup.php
<?php //------------------------------------------------------------------- // View template (BladeOne)の初期設定 //------------------------------------------------------------------- return function():bool { // @now directive \Core\View::directive('now', function() { return "<?php echo date('Y-m-d H:i'); ?>"; }); // @csrf directive \Core\View::directive('csrf', function() { $name = \Core\Foundation\Security\CsrfToken::CSRF_KEY; return '<input name="'.$name.'" type="hidden" value="{{Session(\''.$name.'\')}}">'; }); return true; // Must return true on success };
- @now ディレクティブの後に、@csrf ディレクティブの実装を追加します。
- @csrf ディレクティブでは、ミドルウェアで生成したセッション内のCSRFトークンをInput要素(type=“hidden”)に保存します。
HTMLテンプレートの修正
これまでに作成したフォーム画面のHTMLテンプレートを修正し、@csrfディレクティブを追加します。
ユーザ登録画面
ユーザ登録の新規登録画面と編集画面のテンプレートを修正します。
新規登録画面のテンプレート
/apricot/assets/views/user
- create.blade.php
{{-- コンテンツ --}} @section('content') <form method="POST" name="fm"> @csrf ... </form> @endsection
form
タグの下に@csrf
を追加します。
編集画面のテンプレート
/apricot/assets/views/user
- edit.blade.php
{{-- コンテンツ --}} @section('content') <form method="POST" name="fm"> @csrf ... </form> @endsection
form
タグの下に@csrf
を追加します。
テスト実行
CSRF対策のテストをしてみましょう。Apricotのユーザ編集画面にアクセスします。
■ [保存]ボタンを押して下さい。
■ 正常に保存できます。
■ 先に修正したユーザ編集画面の@csrf
ディレクティブをコメントにして下さい。/apricot/assets/views/user
- create.blade.php
.... <form method="POST" name="fm"> {{--@csrf --}} ... </form> ....
■ ユーザ編集画面で再び[保存]ボタンを押すと次の画面が出ます。
■
VerifyCsrfToken Error
のメッセージと共にWhoopsのエラー画面が出力されます。
■ これは CSRFトークンがサーバーに送信されていないので発生する例外です。
■ 尚、本番用のエラー画面を出力したい場合は、.env のAPP_DEBUG
をfalseに設定してからテストして下さい。- .env
.... APP_DEBUG=false ....
テスト後は、
@csrf
やAPP_DEBUG
を元に戻しておいて下さい。
apricot/ext/csrf.txt · 最終更新: 2020/06/08 11:16 by tanaka
コメント