Ground Sunlight

Windowsで作る - PHPプログラミングの開発環境

ユーザ用ツール

サイト用ツール


サイドバー

メインメニュー

XAMPP アレンジ

IED

WSL2

道具箱

リポジトリ編

フレームワーク編

公開ソフトウェア

メタ
リンク


このページへのアクセス
今日: 2 / 昨日: 1
総計: 1345

apricot:ext:csrf

Apricot CSRF対策

y2sunlight 2020-05-15

Apricot に戻る

関連記事

ミドルウェアを使って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();
    }
}
  • $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
....

テスト後は、@csrfAPP_DEBUG を元に戻しておいて下さい。


コメント

コメントを入力. Wiki文法が有効です:
 
apricot/ext/csrf.txt · 最終更新: 2020/06/08 11:16 by tanaka